Compare commits
133 Commits
feat-find-
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
e93f0d62a6 | ||
|
e4b8fe10ab | ||
|
34828b3240 | ||
|
d2d1e584c1 | ||
7ea9b42452 | |||
c258d4ea92 | |||
|
65a63e09ed | ||
|
16576d3a81 | ||
0a0615813c | |||
a0a6e6dd81 | |||
944e34fa6b | |||
c2c1670ab2 | |||
b2b64dcae0 | |||
2ffa8f6b6d | |||
45e988f7f8 | |||
ca2dec269f | |||
498eeaa0ef | |||
e751f937a2 | |||
6502dde280 | |||
a61e53d56a | |||
a70590d92a | |||
43fa10e3a5 | |||
f7fbdee568 | |||
74ef0c096a | |||
b9a5ad9442 | |||
ae08be8367 | |||
ff717aab73 | |||
0f1d4653a2 | |||
f8e4a668ff | |||
469f74ee6c | |||
36ee8182f9 | |||
bb8b05a2d4 | |||
1dc4202353 | |||
0c6ce9e681 | |||
ccc91b09f6 | |||
9b3d9fd755 | |||
2110b185d2 | |||
|
918106a469 | ||
074ac6e826 | |||
a909013a86 | |||
2929c859b0 | |||
42902f0bd6 | |||
|
24094054ba | ||
886b0985e1 | |||
e57fa4e64a | |||
|
43fee371de | ||
67492b834f | |||
ed050ea820 | |||
faa581e2b4 | |||
9748461024 | |||
1e7a6fd8c5 | |||
|
9fe08cd794 | ||
51a2190559 | |||
8711bca159 | |||
080cc0ab35 | |||
5715f97956 | |||
2bd28f0796 | |||
5c76864595 | |||
3372db8b2b | |||
73262db871 | |||
a4131fe276 | |||
90be28a1f0 | |||
b51c373e96 | |||
1bfb6e9946 | |||
64d0b4b35e | |||
ec3bf67995 | |||
de13c5ed75 | |||
ffaf97e1c7 | |||
f11869bcb8 | |||
7200355f52 | |||
e84e000180 | |||
9fc0ab8554 | |||
820c54b841 | |||
4968f48e8b | |||
a7af608d56 | |||
1e78942572 | |||
d840da2f47 | |||
1c595fc239 | |||
|
ac95894967 | ||
058c086c51 | |||
|
dd59d796f7 | ||
958eb295fb | |||
1185882a17 | |||
92cc51ee8d | |||
4811d03902 | |||
4f933aaf0b | |||
dbcec6125f | |||
719570a83d | |||
723ba56cbe | |||
90ec2d1b4f | |||
e20ccf4b76 | |||
8cceee6668 | |||
68b895786b | |||
|
262d185203 | ||
973d8872b6 | |||
5750e32d97 | |||
8e8d7d534f | |||
94b7d04908 | |||
992931b15c | |||
bb4c535244 | |||
ef487334fb | |||
1b05ed67e7 | |||
19e9ad0cd2 | |||
e53240b417 | |||
f0f2c1e7aa | |||
5742351bb4 | |||
be4ba865eb | |||
fc908386bc | |||
9693da7d37 | |||
0aca296115 | |||
a6cab6e898 | |||
de4059ca50 | |||
85e8ad99a0 | |||
7ca0eab4d7 | |||
e17441a1a9 | |||
38a102bf72 | |||
0c82c4335f | |||
|
2b93eafabd | ||
b94ed37f05 | |||
2c512bae82 | |||
c63bfa89ff | |||
a83cc0faee | |||
8c446a6e81 | |||
16f76be807 | |||
b050575395 | |||
4d5a2f573f | |||
65bf23d76c | |||
e1657ee2d9 | |||
8b5df10131 | |||
cb5be47766 | |||
8ec0353500 | |||
360b85ae33 | |||
a7f3f989bb |
@ -1,5 +1,4 @@
|
|||||||
|
[*]
|
||||||
[*]
|
|
||||||
charset = utf-8-bom
|
charset = utf-8-bom
|
||||||
end_of_line = crlf
|
end_of_line = crlf
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
@ -8,7 +7,7 @@ indent_style = space
|
|||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
# Microsoft .NET properties
|
# Microsoft .NET properties
|
||||||
csharp_style_namespace_declarations=file_scoped:error
|
csharp_style_namespace_declarations = file_scoped:error
|
||||||
csharp_new_line_before_members_in_object_initializers = false
|
csharp_new_line_before_members_in_object_initializers = false
|
||||||
csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:warning
|
csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:warning
|
||||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||||
|
15
.idea/.idea.SPTInstaller/.idea/avalonia.xml
generated
15
.idea/.idea.SPTInstaller/.idea/avalonia.xml
generated
@ -5,11 +5,26 @@
|
|||||||
<map>
|
<map>
|
||||||
<entry key="SPTInstaller/App.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
<entry key="SPTInstaller/App.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
<entry key="SPTInstaller/Assets/Styles.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
<entry key="SPTInstaller/Assets/Styles.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
|
<entry key="SPTInstaller/CustomControls/CacheInfo.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
<entry key="SPTInstaller/CustomControls/DetailedPreCheckItem.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
<entry key="SPTInstaller/CustomControls/DetailedPreCheckItem.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
|
<entry key="SPTInstaller/CustomControls/Dialogs/ChangeLogDialog.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
|
<entry key="SPTInstaller/CustomControls/Dialogs/ConfirmationDialog.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
|
<entry key="SPTInstaller/CustomControls/Dialogs/MessageDialog.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
|
<entry key="SPTInstaller/CustomControls/Dialogs/WhyCacheThoughDialog.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
|
<entry key="SPTInstaller/CustomControls/MainInstallerButton.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
|
<entry key="SPTInstaller/CustomControls/PreCheckDetails.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
<entry key="SPTInstaller/CustomControls/PreCheckItem.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
<entry key="SPTInstaller/CustomControls/PreCheckItem.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
|
<entry key="SPTInstaller/CustomControls/ProgressableTaskItem.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
||||||
<entry key="SPTInstaller/CustomControls/SPTInstallButton.axaml" value="SPTInstaller/SPTInstaller.csproj" />
|
<entry key="SPTInstaller/CustomControls/SPTInstallButton.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/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>
|
||||||
|
24
README.md
24
README.md
@ -1,20 +1,20 @@
|
|||||||
# SPT-AKI Installer made for EFT.
|
# SPT Installer made for EFT.
|
||||||
|
|
||||||
![Installer Image](https://media.discordapp.net/attachments/875707258074447904/1107352250705268807/image.png?width=1148&height=671)
|
<img src="https://i.imgur.com/jtlwLsr.png" alt="spt installer 2.59" width="700"/>
|
||||||
|
|
||||||
### Pre install checks:
|
### Pre install checks:
|
||||||
- Checks if .net 4.7.2 (or higher) is installed
|
- Checks if .net 4.7.2 (or higher) is installed
|
||||||
- Checks if .net 6 desktop runtime is installed
|
- Checks if .net 8 runtime is installed
|
||||||
- Checks if EFT is installed,
|
- Checks if EFT is installed
|
||||||
- Checks if there is enough space before install,
|
- Checks if there is enough space before install
|
||||||
- Checks installer is not in OG game directory,
|
- 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
|
@ -5,7 +5,7 @@
|
|||||||
xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
|
xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
|
||||||
RequestedThemeVariant="Light">
|
RequestedThemeVariant="Light">
|
||||||
<Application.DataTemplates>
|
<Application.DataTemplates>
|
||||||
<local:ViewLocator/>
|
<local:ViewLocator />
|
||||||
</Application.DataTemplates>
|
</Application.DataTemplates>
|
||||||
|
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
@ -13,36 +13,43 @@
|
|||||||
<dialogHostAvalonia:DialogHostStyles />
|
<dialogHostAvalonia:DialogHostStyles />
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
|
|
||||||
<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="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_Lighter" Color="Gainsboro"/>
|
<SolidColorBrush x:Key="SPT_Brush_LightGrayBlue" Color="{StaticResource SPT_LightGrayBlue}" />
|
||||||
|
<SolidColorBrush x:Key="SPT_Brush_Lighter" Color="Gainsboro" />
|
||||||
|
|
||||||
<!-- Path Geometry -->
|
<!-- Path Geometry -->
|
||||||
<PathGeometry x:Key="CircledCheck" Figures="M 9.0646825 0.06313182 C 7.3648066 0.28806336 5.7978836 0.78839047 4.3639137 1.7461752 3.2921695 2.4620115 2.3631641 3.4084722 1.6479106 4.4762536 0.98737415 5.4623497 0.47819447 6.5896932 0.22806644 7.7524208 -1.2315929 14.537597 4.5254007 20.882361 11.493416 19.89881 c 1.391191 -0.196414 2.73334 -0.717402 3.917306 -1.463979 1.003459 -0.632768 1.91619 -1.463899 2.626322 -2.413989 C 22.172937 10.487163 19.448371 2.526326 12.903647 0.44688781 11.70918 0.06738309 10.312268 -0.10195753 9.0646825 0.06313182 M 14.235529 6.538212 c 0.719844 -0.1922804 1.369569 0.5544499 0.96037 1.2142088 -0.345429 0.5568703 -0.967577 1.0212266 -1.430447 1.4820746 L 10.94499 12.042639 C 10.500924 12.484766 9.9264114 13.323465 9.299721 13.490862 8.8023811 13.623702 8.452016 13.299829 8.1245295 12.978374 7.342478 12.210582 6.3754514 11.44552 5.7298007 10.560564 5.480503 10.218905 5.4699265 9.723192 5.7920077 9.4212362 6.6694846 8.5988409 7.8158456 10.253773 8.3595682 10.794576 c 0.1820751 0.181125 0.4825335 0.608587 0.7834627 0.52988 0.4212659 -0.110141 0.8750481 -0.777076 1.1751921 -1.075907 L 12.981992 7.5964118 C 13.331649 7.2482817 13.738346 6.6710512 14.235529 6.538212 Z"
|
<PathGeometry x:Key="CircledCheck"
|
||||||
FillRule="NonZero"
|
Figures="M 9.0646825 0.06313182 C 7.3648066 0.28806336 5.7978836 0.78839047 4.3639137 1.7461752 3.2921695 2.4620115 2.3631641 3.4084722 1.6479106 4.4762536 0.98737415 5.4623497 0.47819447 6.5896932 0.22806644 7.7524208 -1.2315929 14.537597 4.5254007 20.882361 11.493416 19.89881 c 1.391191 -0.196414 2.73334 -0.717402 3.917306 -1.463979 1.003459 -0.632768 1.91619 -1.463899 2.626322 -2.413989 C 22.172937 10.487163 19.448371 2.526326 12.903647 0.44688781 11.70918 0.06738309 10.312268 -0.10195753 9.0646825 0.06313182 M 14.235529 6.538212 c 0.719844 -0.1922804 1.369569 0.5544499 0.96037 1.2142088 -0.345429 0.5568703 -0.967577 1.0212266 -1.430447 1.4820746 L 10.94499 12.042639 C 10.500924 12.484766 9.9264114 13.323465 9.299721 13.490862 8.8023811 13.623702 8.452016 13.299829 8.1245295 12.978374 7.342478 12.210582 6.3754514 11.44552 5.7298007 10.560564 5.480503 10.218905 5.4699265 9.723192 5.7920077 9.4212362 6.6694846 8.5988409 7.8158456 10.253773 8.3595682 10.794576 c 0.1820751 0.181125 0.4825335 0.608587 0.7834627 0.52988 0.4212659 -0.110141 0.8750481 -0.777076 1.1751921 -1.075907 L 12.981992 7.5964118 C 13.331649 7.2482817 13.738346 6.6710512 14.235529 6.538212 Z"
|
||||||
/>
|
FillRule="NonZero" />
|
||||||
<PathGeometry x:Key="CircledX" Figures="M 9.3972738 0.04245969 C 7.0827527 0.34574986 5.0318949 1.0076069 3.2592046 2.6077382 2.3324496 3.4442761 1.5788823 4.453119 1.0210803 5.566508 c -2.7620688 5.513177 0.320857 12.50987 6.3432023 14.090152 1.2916144 0.338914 2.6250608 0.43131 3.9486944 0.2576 4.747034 -0.622966 8.468465 -4.700542 8.677783 -9.470458 C 20.19922 5.6929665 16.858078 1.3284705 12.212185 0.27365016 11.325097 0.07224603 10.306294 -0.0766601 9.3972738 0.04245969 M 6.8951311 6.0962212 c 0.4071071 0.00285 0.6713562 0.2964224 0.938304 0.5628006 l 1.4856467 1.4826978 c 0.158768 0.1584535 0.4033136 0.5189835 0.6255352 0.5771984 0.126202 0.033088 0.234732 -0.1102653 0.312768 -0.1870931 0.249277 -0.2453478 0.495229 -0.4941674 0.742824 -0.7412705 l 1.094688 -1.0925142 c 0.20842 -0.2080069 0.414066 -0.4608068 0.703728 -0.554646 0.666547 -0.2158494 1.280275 0.3966607 1.063998 1.0618847 -0.141137 0.4339627 -0.633395 0.779821 -0.94671 1.092514 L 11.664841 9.54638 c -0.107944 0.1077293 -0.38877 0.3007922 -0.38877 0.46822 0 0.222015 0.475094 0.554373 0.623346 0.70233 0.595784 0.594602 1.420358 1.20083 1.871407 1.9119 0.241614 0.380976 0.158691 0.920717 -0.229532 1.171722 -0.308154 0.199227 -0.730468 0.156386 -1.016338 -0.06095 C 11.870017 13.24165 11.308677 12.546499 10.726537 11.965515 10.582664 11.821928 10.166604 11.252104 9.944617 11.31028 9.7223954 11.3685 9.4778498 11.729025 9.3190818 11.887479 l -1.4856467 1.4827 C 7.520315 13.682638 7.2187683 13.989088 6.7387474 13.917412 6.1917559 13.835786 5.9203119 13.23264 6.1340103 12.745886 6.2635359 12.450907 6.5544494 12.227488 6.7778435 12.004537 L 8.0289153 10.755949 C 8.183657 10.601515 8.691357 10.24512 8.691357 10.0146 c 0 -0.2305204 -0.5077 -0.5869139 -0.6624417 -0.7413489 L 6.7778435 8.0246649 C 6.5544494 7.8017132 6.2635359 7.5782948 6.1340103 7.2833157 5.8906767 6.7289817 6.2771407 6.091851 6.8951311 6.0962212 Z" FillRule="NonZero"
|
<PathGeometry x:Key="CircledX"
|
||||||
/>
|
Figures="M 9.3972738 0.04245969 C 7.0827527 0.34574986 5.0318949 1.0076069 3.2592046 2.6077382 2.3324496 3.4442761 1.5788823 4.453119 1.0210803 5.566508 c -2.7620688 5.513177 0.320857 12.50987 6.3432023 14.090152 1.2916144 0.338914 2.6250608 0.43131 3.9486944 0.2576 4.747034 -0.622966 8.468465 -4.700542 8.677783 -9.470458 C 20.19922 5.6929665 16.858078 1.3284705 12.212185 0.27365016 11.325097 0.07224603 10.306294 -0.0766601 9.3972738 0.04245969 M 6.8951311 6.0962212 c 0.4071071 0.00285 0.6713562 0.2964224 0.938304 0.5628006 l 1.4856467 1.4826978 c 0.158768 0.1584535 0.4033136 0.5189835 0.6255352 0.5771984 0.126202 0.033088 0.234732 -0.1102653 0.312768 -0.1870931 0.249277 -0.2453478 0.495229 -0.4941674 0.742824 -0.7412705 l 1.094688 -1.0925142 c 0.20842 -0.2080069 0.414066 -0.4608068 0.703728 -0.554646 0.666547 -0.2158494 1.280275 0.3966607 1.063998 1.0618847 -0.141137 0.4339627 -0.633395 0.779821 -0.94671 1.092514 L 11.664841 9.54638 c -0.107944 0.1077293 -0.38877 0.3007922 -0.38877 0.46822 0 0.222015 0.475094 0.554373 0.623346 0.70233 0.595784 0.594602 1.420358 1.20083 1.871407 1.9119 0.241614 0.380976 0.158691 0.920717 -0.229532 1.171722 -0.308154 0.199227 -0.730468 0.156386 -1.016338 -0.06095 C 11.870017 13.24165 11.308677 12.546499 10.726537 11.965515 10.582664 11.821928 10.166604 11.252104 9.944617 11.31028 9.7223954 11.3685 9.4778498 11.729025 9.3190818 11.887479 l -1.4856467 1.4827 C 7.520315 13.682638 7.2187683 13.989088 6.7387474 13.917412 6.1917559 13.835786 5.9203119 13.23264 6.1340103 12.745886 6.2635359 12.450907 6.5544494 12.227488 6.7778435 12.004537 L 8.0289153 10.755949 C 8.183657 10.601515 8.691357 10.24512 8.691357 10.0146 c 0 -0.2305204 -0.5077 -0.5869139 -0.6624417 -0.7413489 L 6.7778435 8.0246649 C 6.5544494 7.8017132 6.2635359 7.5782948 6.1340103 7.2833157 5.8906767 6.7289817 6.2771407 6.091851 6.8951311 6.0962212 Z"
|
||||||
<PathGeometry x:Key="CircledWarn" Figures="M 9.4328769 0.04019892 C 7.0982838 0.34605265 4.9864964 0.9947734 3.2144923 2.6416847 -0.51716071 6.1098902 -1.0931625 11.937378 1.9776592 16.023146 c 0.6666064 0.886919 1.4893703 1.657003 2.4101133 2.276223 0.9414784 0.633151 1.9874973 1.110834 3.0896371 1.387001 1.2547837 0.314488 2.5508664 0.396281 3.8327134 0.22809 4.776463 -0.626673 8.470809 -4.727503 8.680748 -9.510654 C 20.200809 5.6204614 16.766697 1.2560532 12.09231 0.24958776 11.252125 0.06868514 10.291794 -0.07233093 9.4328769 0.04019892 M 9.784861 4.2119583 c 0.934085 -0.1667851 1.016841 0.6682722 1.016841 1.352973 v 5.1120367 c 0 0.62605 0.190424 1.671637 -0.664858 1.821253 C 9.2424915 12.654703 9.1591106 11.781677 9.1591106 11.145248 V 6.0332096 c 0 -0.6032595 -0.201139 -1.6735876 0.6257504 -1.8212513 m 0 9.9899357 c 1.062442 -0.189692 1.447474 1.424659 0.391092 1.613298 -1.0624433 0.189692 -1.4474743 -1.424659 -0.391092 -1.613298 z" FillRule="NonZero"
|
FillRule="NonZero" />
|
||||||
/>
|
<PathGeometry x:Key="CircledWarn"
|
||||||
<PathGeometry x:Key="Cache" Figures="M 5.4515409 18.707476 C 2.8719183 18.411612 0.6356275 17.508046 0.12886774 16.556868 0.011421 16.336424 0 16.17685 0 14.75659 v -1.558391 l 0.20348761 0.163959 c 0.89061819 0.717606 2.90294209 1.394851 4.91765599 1.655024 1.0475327 0.135279 3.5115239 0.155131 4.4933927 0.03621 2.1668367 -0.262447 3.9985147 -0.826312 5.0629357 -1.558573 l 0.322527 -0.221877 v 1.524757 c 0 1.755973 -0.0041 1.772626 -0.56529 2.263709 -0.796666 0.697209 -2.144167 1.203544 -4.0933 1.53809 -0.5767809 0.099 -1.1020996 0.129287 -2.5440527 0.146688 -0.9994482 0.01206 -2.0550657 -0.0054 -2.3458154 -0.0387 z M 7.8601306 17.28745 c 0.0527 -0.04941 0.095816 -0.193581 0.095816 -0.320366 0 -0.405037 -0.46603 -0.61585 -0.7533032 -0.340767 -0.1321068 0.126504 -0.1782781 0.469731 -0.081588 0.606525 0.1545094 0.218594 0.5342719 0.246654 0.7390756 0.05461 z M 5.5458634 17.092351 c 0.075327 -0.0917 0.1369546 -0.21599 0.1369546 -0.276208 0 -0.203883 -0.2583299 -0.442927 -0.478668 -0.442927 -0.2881074 0 -0.4464405 0.159294 -0.4464405 0.449148 0 0.424533 0.5160004 0.601292 0.7881539 0.269987 z m 4.7625076 0.04017 c 0.07342 -0.0703 0.132157 -0.210917 0.132157 -0.316377 0 -0.105459 -0.05874 -0.24607 -0.132157 -0.316375 -0.07342 -0.0703 -0.220267 -0.126552 -0.3303979 -0.126552 -0.1101318 0 -0.2569762 0.05625 -0.3303975 0.126552 -0.073421 0.0703 -0.1321569 0.210916 -0.1321569 0.316375 0 0.10546 0.058738 0.246072 0.1321569 0.316377 0.073421 0.07031 0.2202657 0.126551 0.3303975 0.126551 0.1101309 0 0.2569759 -0.05624 0.3303979 -0.126551 z M 6.4427306 14.219282 C 3.438293 14.06843 0.69941831 13.09766 0.13645011 11.98407 0.00570426 11.725447 -0.00143606 11.622911 0.01527712 10.244516 L 0.03303424 8.7797875 0.36343157 9.0065262 C 2.6575042 10.580863 7.8536269 11.112097 11.747571 10.170399 12.962111 9.8766787 13.852599 9.520745 14.620039 9.0222575 l 0.379955 -0.2467994 v 1.4852079 c 0 0.822685 -0.02906 1.558392 -0.06514 1.649265 -0.108197 0.272498 -0.66301 0.788548 -1.121691 1.043315 -1.168844 0.64922 -3.065811 1.13473 -4.8263882 1.235267 -1.2569769 0.07178 -1.6359325 0.07636 -2.5440532 0.03076 z m 1.4159088 -1.492928 c 0.075324 -0.0917 0.1369543 -0.215989 0.1369543 -0.276207 0 -0.203883 -0.2583318 -0.442926 -0.4786675 -0.442926 -0.2881093 0 -0.4464425 0.159291 -0.4464425 0.449146 0 0.424534 0.5160019 0.601293 0.7881557 0.269987 z m -2.29146 -0.159368 c 0.1618895 -0.183199 0.1439776 -0.411835 -0.046556 -0.594283 -0.3090962 -0.29598 -0.7629143 -0.107341 -0.7629143 0.317126 0 0.426014 0.5204584 0.604217 0.8094703 0.277157 z m 4.6992386 0.06839 c 0.12628 -0.09512 0.17411 -0.18946 0.17411 -0.343424 0 -0.232297 -0.242217 -0.474564 -0.4744705 -0.474564 -0.1820319 0 -0.4506387 0.282868 -0.4506387 0.474564 0 0.191699 0.2686068 0.474567 0.4506387 0.474567 0.069437 0 0.2046015 -0.05901 0.3003605 -0.131143 z M 5.9140951 9.6877494 C 3.2886712 9.3885406 1.5607385 8.8497481 0.58264334 8.0253262 0.03362115 7.5625637 0 7.4352112 0 5.8183118 V 4.4150124 L 0.30935351 4.6411272 C 1.3355178 5.3911787 3.3497821 5.9987632 5.5506611 6.2221182 6.7548513 6.3443244 9.00242 6.3139162 10.077093 6.1608788 12.027869 5.8830787 13.636 5.3630162 14.620197 4.6916622 14.792913 4.5738469 14.949026 4.4774523 14.967113 4.4774523 14.985194 4.4774523 15 5.1050107 15 5.8720299 15 7.4791794 14.982469 7.5435731 14.411053 8.0355275 13.533871 8.7907237 11.69734 9.3913644 9.5158238 9.6365256 8.7752594 9.71975 6.4811675 9.7523775 5.9140951 9.6877506 Z M 7.7751038 8.4969125 C 8.1763294 8.227805 7.8985219 7.5555425 7.4323869 7.6675706 7.1999381 7.7234394 7.0703688 7.8802994 7.071495 8.104485 7.072745 8.3355694 7.11687 8.4143437 7.3017606 8.5147362 7.4816994 8.6124437 7.610055 8.6076106 7.7751038 8.4969175 Z M 5.5458634 8.2338012 C 5.6211901 8.1421037 5.682818 8.0178119 5.682818 7.9575956 c 0 -0.1899694 -0.2585097 -0.4429287 -0.4526467 -0.4429287 -0.2913367 0 -0.4724618 0.1632037 -0.4724618 0.425715 0 0.2931318 0.1578686 0.4601406 0.4349596 0.4601406 0.154904 0 0.2550881 -0.047287 0.3531943 -0.1667213 z m 4.7625076 0.040175 c 0.07342 -0.070305 0.132157 -0.2109162 0.132157 -0.316375 0 -0.1054612 -0.05874 -0.2460718 -0.132157 -0.3163775 -0.286005 -0.2738662 -0.7929522 -0.071601 -0.7929522 0.3163775 0 0.1054588 0.058737 0.2460694 0.1321568 0.316375 0.073421 0.070304 0.2202657 0.1265519 0.3303975 0.1265519 0.1101309 0 0.2569759 -0.056244 0.3303979 -0.1265519 z M 6.178414 5.3866444 C 3.8630228 5.2033161 2.0080132 4.6795372 0.82192238 3.8741859 0.26331487 3.4948944 0.03303924 3.1533125 0.03303924 2.7039893 c 0 -0.2939821 0.03374352 -0.3937029 0.21156697 -0.6252455 C 1.4428273 0.518552 5.8324898 -0.36375235 9.8724944 0.14357696 12.82408 0.51422624 14.999999 1.6015126 14.999999 2.7057413 c 0 1.1212199 -2.227472 2.2145851 -5.2863421 2.5948303 C 9.1252344 5.3737159 6.74658 5.4316311 6.178414 5.3866444 Z" FillRule="NonZero"
|
Figures="M 9.4328769 0.04019892 C 7.0982838 0.34605265 4.9864964 0.9947734 3.2144923 2.6416847 -0.51716071 6.1098902 -1.0931625 11.937378 1.9776592 16.023146 c 0.6666064 0.886919 1.4893703 1.657003 2.4101133 2.276223 0.9414784 0.633151 1.9874973 1.110834 3.0896371 1.387001 1.2547837 0.314488 2.5508664 0.396281 3.8327134 0.22809 4.776463 -0.626673 8.470809 -4.727503 8.680748 -9.510654 C 20.200809 5.6204614 16.766697 1.2560532 12.09231 0.24958776 11.252125 0.06868514 10.291794 -0.07233093 9.4328769 0.04019892 M 9.784861 4.2119583 c 0.934085 -0.1667851 1.016841 0.6682722 1.016841 1.352973 v 5.1120367 c 0 0.62605 0.190424 1.671637 -0.664858 1.821253 C 9.2424915 12.654703 9.1591106 11.781677 9.1591106 11.145248 V 6.0332096 c 0 -0.6032595 -0.201139 -1.6735876 0.6257504 -1.8212513 m 0 9.9899357 c 1.062442 -0.189692 1.447474 1.424659 0.391092 1.613298 -1.0624433 0.189692 -1.4474743 -1.424659 -0.391092 -1.613298 z"
|
||||||
/>
|
FillRule="NonZero" />
|
||||||
|
<PathGeometry x:Key="Cache"
|
||||||
|
Figures="M 5.4515409 18.707476 C 2.8719183 18.411612 0.6356275 17.508046 0.12886774 16.556868 0.011421 16.336424 0 16.17685 0 14.75659 v -1.558391 l 0.20348761 0.163959 c 0.89061819 0.717606 2.90294209 1.394851 4.91765599 1.655024 1.0475327 0.135279 3.5115239 0.155131 4.4933927 0.03621 2.1668367 -0.262447 3.9985147 -0.826312 5.0629357 -1.558573 l 0.322527 -0.221877 v 1.524757 c 0 1.755973 -0.0041 1.772626 -0.56529 2.263709 -0.796666 0.697209 -2.144167 1.203544 -4.0933 1.53809 -0.5767809 0.099 -1.1020996 0.129287 -2.5440527 0.146688 -0.9994482 0.01206 -2.0550657 -0.0054 -2.3458154 -0.0387 z M 7.8601306 17.28745 c 0.0527 -0.04941 0.095816 -0.193581 0.095816 -0.320366 0 -0.405037 -0.46603 -0.61585 -0.7533032 -0.340767 -0.1321068 0.126504 -0.1782781 0.469731 -0.081588 0.606525 0.1545094 0.218594 0.5342719 0.246654 0.7390756 0.05461 z M 5.5458634 17.092351 c 0.075327 -0.0917 0.1369546 -0.21599 0.1369546 -0.276208 0 -0.203883 -0.2583299 -0.442927 -0.478668 -0.442927 -0.2881074 0 -0.4464405 0.159294 -0.4464405 0.449148 0 0.424533 0.5160004 0.601292 0.7881539 0.269987 z m 4.7625076 0.04017 c 0.07342 -0.0703 0.132157 -0.210917 0.132157 -0.316377 0 -0.105459 -0.05874 -0.24607 -0.132157 -0.316375 -0.07342 -0.0703 -0.220267 -0.126552 -0.3303979 -0.126552 -0.1101318 0 -0.2569762 0.05625 -0.3303975 0.126552 -0.073421 0.0703 -0.1321569 0.210916 -0.1321569 0.316375 0 0.10546 0.058738 0.246072 0.1321569 0.316377 0.073421 0.07031 0.2202657 0.126551 0.3303975 0.126551 0.1101309 0 0.2569759 -0.05624 0.3303979 -0.126551 z M 6.4427306 14.219282 C 3.438293 14.06843 0.69941831 13.09766 0.13645011 11.98407 0.00570426 11.725447 -0.00143606 11.622911 0.01527712 10.244516 L 0.03303424 8.7797875 0.36343157 9.0065262 C 2.6575042 10.580863 7.8536269 11.112097 11.747571 10.170399 12.962111 9.8766787 13.852599 9.520745 14.620039 9.0222575 l 0.379955 -0.2467994 v 1.4852079 c 0 0.822685 -0.02906 1.558392 -0.06514 1.649265 -0.108197 0.272498 -0.66301 0.788548 -1.121691 1.043315 -1.168844 0.64922 -3.065811 1.13473 -4.8263882 1.235267 -1.2569769 0.07178 -1.6359325 0.07636 -2.5440532 0.03076 z m 1.4159088 -1.492928 c 0.075324 -0.0917 0.1369543 -0.215989 0.1369543 -0.276207 0 -0.203883 -0.2583318 -0.442926 -0.4786675 -0.442926 -0.2881093 0 -0.4464425 0.159291 -0.4464425 0.449146 0 0.424534 0.5160019 0.601293 0.7881557 0.269987 z m -2.29146 -0.159368 c 0.1618895 -0.183199 0.1439776 -0.411835 -0.046556 -0.594283 -0.3090962 -0.29598 -0.7629143 -0.107341 -0.7629143 0.317126 0 0.426014 0.5204584 0.604217 0.8094703 0.277157 z m 4.6992386 0.06839 c 0.12628 -0.09512 0.17411 -0.18946 0.17411 -0.343424 0 -0.232297 -0.242217 -0.474564 -0.4744705 -0.474564 -0.1820319 0 -0.4506387 0.282868 -0.4506387 0.474564 0 0.191699 0.2686068 0.474567 0.4506387 0.474567 0.069437 0 0.2046015 -0.05901 0.3003605 -0.131143 z M 5.9140951 9.6877494 C 3.2886712 9.3885406 1.5607385 8.8497481 0.58264334 8.0253262 0.03362115 7.5625637 0 7.4352112 0 5.8183118 V 4.4150124 L 0.30935351 4.6411272 C 1.3355178 5.3911787 3.3497821 5.9987632 5.5506611 6.2221182 6.7548513 6.3443244 9.00242 6.3139162 10.077093 6.1608788 12.027869 5.8830787 13.636 5.3630162 14.620197 4.6916622 14.792913 4.5738469 14.949026 4.4774523 14.967113 4.4774523 14.985194 4.4774523 15 5.1050107 15 5.8720299 15 7.4791794 14.982469 7.5435731 14.411053 8.0355275 13.533871 8.7907237 11.69734 9.3913644 9.5158238 9.6365256 8.7752594 9.71975 6.4811675 9.7523775 5.9140951 9.6877506 Z M 7.7751038 8.4969125 C 8.1763294 8.227805 7.8985219 7.5555425 7.4323869 7.6675706 7.1999381 7.7234394 7.0703688 7.8802994 7.071495 8.104485 7.072745 8.3355694 7.11687 8.4143437 7.3017606 8.5147362 7.4816994 8.6124437 7.610055 8.6076106 7.7751038 8.4969175 Z M 5.5458634 8.2338012 C 5.6211901 8.1421037 5.682818 8.0178119 5.682818 7.9575956 c 0 -0.1899694 -0.2585097 -0.4429287 -0.4526467 -0.4429287 -0.2913367 0 -0.4724618 0.1632037 -0.4724618 0.425715 0 0.2931318 0.1578686 0.4601406 0.4349596 0.4601406 0.154904 0 0.2550881 -0.047287 0.3531943 -0.1667213 z m 4.7625076 0.040175 c 0.07342 -0.070305 0.132157 -0.2109162 0.132157 -0.316375 0 -0.1054612 -0.05874 -0.2460718 -0.132157 -0.3163775 -0.286005 -0.2738662 -0.7929522 -0.071601 -0.7929522 0.3163775 0 0.1054588 0.058737 0.2460694 0.1321568 0.316375 0.073421 0.070304 0.2202657 0.1265519 0.3303975 0.1265519 0.1101309 0 0.2569759 -0.056244 0.3303979 -0.1265519 z M 6.178414 5.3866444 C 3.8630228 5.2033161 2.0080132 4.6795372 0.82192238 3.8741859 0.26331487 3.4948944 0.03303924 3.1533125 0.03303924 2.7039893 c 0 -0.2939821 0.03374352 -0.3937029 0.21156697 -0.6252455 C 1.4428273 0.518552 5.8324898 -0.36375235 9.8724944 0.14357696 12.82408 0.51422624 14.999999 1.6015126 14.999999 2.7057413 c 0 1.1212199 -2.227472 2.2145851 -5.2863421 2.5948303 C 9.1252344 5.3737159 6.74658 5.4316311 6.178414 5.3866444 Z"
|
||||||
|
FillRule="NonZero" />
|
||||||
<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"
|
||||||
</Application.Resources>
|
FillRule="NonZero" />
|
||||||
</Application>
|
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
@ -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,36 @@ 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()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
@ -20,44 +45,55 @@ 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) =>
|
||||||
{
|
{
|
||||||
Log.Error(exception, "An application exception occurred");
|
Log.Error(exception, "An application exception occurred");
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,271 +1,297 @@
|
|||||||
<Styles xmlns="https://github.com/avaloniaui"
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
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 SPT_Background_Dark}">
|
||||||
<StackPanel Spacing="5" Background="{StaticResource AKI_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" />
|
||||||
</StackPanel>
|
<CheckBox Content="sldkflskdf" />
|
||||||
</Design.PreviewWith>
|
</StackPanel>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
<!-- Add Styles Here -->
|
<!-- Add Styles Here -->
|
||||||
|
|
||||||
<!-- 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">
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="DimGray"/>
|
<Setter Property="BorderBrush" Value="DimGray" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="TextBox:pointerover /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
|
<Style
|
||||||
<Setter Property="Foreground" Value="DimGray"/>
|
Selector="TextBox:pointerover /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
|
||||||
</Style>
|
<Setter Property="Foreground" Value="DimGray" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="TextBox:focus /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
|
<Style
|
||||||
<Setter Property="Foreground" Value="DimGray"/>
|
Selector="TextBox:focus /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
|
||||||
</Style>
|
<Setter Property="Foreground" Value="DimGray" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="TextBox /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
|
<Style
|
||||||
<Setter Property="Foreground" Value="White"/>
|
Selector="TextBox /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
|
||||||
</Style>
|
<Setter Property="Foreground" Value="White" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<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">
|
||||||
<Setter Property="Foreground" Value="DimGray"/>
|
<Setter Property="Foreground" Value="DimGray" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Label.versionMismatch">
|
<Style Selector="Label.versionMismatch">
|
||||||
<Setter Property="Foreground" Value="OrangeRed"/>
|
<Setter Property="Foreground" Value="OrangeRed" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 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">
|
||||||
<Setter Property="Foreground" Value="Red"/>
|
<Setter Property="Foreground" Value="Red" />
|
||||||
<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%">
|
||||||
<Setter Property="Foreground" Value="Red"/>
|
<Setter Property="Foreground" Value="Red" />
|
||||||
<Setter Property="Value" Value="100"/>
|
<Setter Property="Value" Value="100" />
|
||||||
</KeyFrame>
|
</KeyFrame>
|
||||||
</Animation>
|
</Animation>
|
||||||
</Style.Animations>
|
</Style.Animations>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 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_Background_Dark}"/>
|
<Setter Property="Foreground" Value="{StaticResource SPT_White}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Button:pointerover">
|
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
<Setter Property="Background" Value="{StaticResource SPT_LightGrayBlue}" />
|
||||||
</Style>
|
<Setter Property="BorderBrush" Value="{StaticResource SPT_LightGrayBlue}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SPT_White}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
<Style Selector="Button:pressed /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}"/>
|
<Setter Property="Background" Value="{StaticResource SPT_Brush_Yellow}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
|
</Style>
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="Button:pressed /template/ ContentPresenter">
|
<Style Selector="Button:disabled /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}"/>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
</Style>
|
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Button:disabled /template/ ContentPresenter">
|
<!-- Button yellow -->
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
<Style Selector="Button.yellow">
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
|
<Setter Property="Background" Value="{StaticResource SPT_Brush_Yellow}" />
|
||||||
</Style>
|
<Setter Property="Foreground" Value="{StaticResource SPT_Background_Dark}" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- Button yellow -->
|
<Style Selector="Button.yellow:pointerover">
|
||||||
<Style Selector="Button.yellow">
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}"/>
|
</Style>
|
||||||
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="Button.yellow:pointerover">
|
<Style Selector="Button.yellow:pointerover /template/ ContentPresenter">
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
<Setter Property="Background" Value="Gold" />
|
||||||
</Style>
|
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SPT_Background_Dark}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Button.yellow:pointerover /template/ ContentPresenter">
|
<Style Selector="Button.yellow:pressed /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="Gold"/>
|
<Setter Property="Background" Value="{StaticResource SPT_Brush_Lighter}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
|
</Style>
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="Button.yellow:pressed /template/ ContentPresenter">
|
<Style Selector="Button.yellow:disabled /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="{StaticResource AKI_Brush_Lighter}"/>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
</Style>
|
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
|
||||||
|
</Style>
|
||||||
<Style Selector="Button.yellow:disabled /template/ ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
|
|
||||||
</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" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 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" />
|
||||||
<Setter Property="ContentTemplate">
|
<Setter Property="ContentTemplate">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding $parent[Button].Content}" TextDecorations="Underline"/>
|
<TextBlock Text="{Binding $parent[Button].Content}" TextDecorations="Underline" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</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" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 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">
|
||||||
|
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Button.outlinedTLCorner:pressed /template/ ContentPresenter">
|
|
||||||
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
|
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
|
||||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Button icon Style -->
|
<!-- Button icon Style -->
|
||||||
<Style Selector="Button.icon">
|
<Style Selector="Button.icon">
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="Foreground" Value="White"/>
|
<Setter Property="Foreground" Value="White" />
|
||||||
</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 |
BIN
SPTInstaller/Assets/spt_installer.ico
Normal file
BIN
SPTInstaller/Assets/spt_installer.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 156 KiB |
@ -5,13 +5,14 @@ namespace SPTInstaller.Behaviors;
|
|||||||
|
|
||||||
public class SpanBehavior : AvaloniaObject
|
public class SpanBehavior : AvaloniaObject
|
||||||
{
|
{
|
||||||
public static readonly AttachedProperty<bool> SpanProperty = AvaloniaProperty.RegisterAttached<SpanBehavior, Interactive, bool>("Span");
|
public static readonly AttachedProperty<bool> SpanProperty =
|
||||||
|
AvaloniaProperty.RegisterAttached<SpanBehavior, Interactive, bool>("Span");
|
||||||
|
|
||||||
public static void SetSpan(AvaloniaObject element, bool value)
|
public static void SetSpan(AvaloniaObject element, bool value)
|
||||||
{
|
{
|
||||||
element.SetValue(SpanProperty, value);
|
element.SetValue(SpanProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool GetSpan(AvaloniaObject element)
|
public static bool GetSpan(AvaloniaObject element)
|
||||||
{
|
{
|
||||||
return element.GetValue(SpanProperty);
|
return element.GetValue(SpanProperty);
|
||||||
|
@ -12,16 +12,16 @@ public class InstallController
|
|||||||
{
|
{
|
||||||
public event EventHandler RecheckRequested = delegate { };
|
public event EventHandler RecheckRequested = delegate { };
|
||||||
public event EventHandler<IProgressableTask> TaskChanged = delegate { };
|
public event EventHandler<IProgressableTask> TaskChanged = delegate { };
|
||||||
|
|
||||||
private bool _installRunning = false;
|
private bool _installRunning = false;
|
||||||
private IPreCheck[] _preChecks { get; set; }
|
private IPreCheck[] _preChecks { get; set; }
|
||||||
private IProgressableTask[] _tasks { get; set; }
|
private IProgressableTask[] _tasks { get; set; }
|
||||||
|
|
||||||
public InstallController(IProgressableTask[] tasks, IPreCheck[] preChecks = null)
|
public InstallController(IProgressableTask[] tasks, IPreCheck[] preChecks = null)
|
||||||
{
|
{
|
||||||
_tasks = tasks;
|
_tasks = tasks;
|
||||||
_preChecks = preChecks;
|
_preChecks = preChecks;
|
||||||
|
|
||||||
foreach (var check in _preChecks)
|
foreach (var check in _preChecks)
|
||||||
{
|
{
|
||||||
check.ReeevaluationRequested += (s, _) =>
|
check.ReeevaluationRequested += (s, _) =>
|
||||||
@ -32,63 +32,64 @@ public class InstallController
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log.Information($"{preCheck.Name}: requested re-evaluation");
|
Log.Information($"{preCheck.Name}: requested re-evaluation");
|
||||||
|
|
||||||
if (_installRunning)
|
if (_installRunning)
|
||||||
{
|
{
|
||||||
Log.Warning("Install is running, re-evaluation denied (how did you do this?)");
|
Log.Warning("Install is running, re-evaluation denied (how did you do this?)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RecheckRequested?.Invoke(this, null);
|
RecheckRequested?.Invoke(this, null);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> RunPreChecks()
|
public async Task<IResult> RunPreChecks()
|
||||||
{
|
{
|
||||||
Log.Information("-<>--<>- Running PreChecks -<>--<>-");
|
Log.Information("-<>--<>- Running PreChecks -<>--<>-");
|
||||||
var requiredResults = new List<IResult>();
|
var requiredResults = new List<IResult>();
|
||||||
|
|
||||||
foreach (var check in _preChecks)
|
foreach (var check in _preChecks)
|
||||||
{
|
{
|
||||||
check.State = StatusSpinner.SpinnerState.Pending;
|
check.State = StatusSpinner.SpinnerState.Pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var check in _preChecks)
|
foreach (var check in _preChecks)
|
||||||
{
|
{
|
||||||
var result = await check.RunCheck();
|
var result = await check.RunCheck();
|
||||||
|
|
||||||
Log.Information($"PreCheck: {check.Name} ({(check.IsRequired ? "Required" : "Optional")}) -> {(result.Succeeded ? "Passed" : "Failed")}\nDetail: {check.PreCheckDetails.ReplaceLineEndings(" ")}");
|
Log.Information(
|
||||||
|
$"PreCheck: {check.Name} ({(check.IsRequired ? "Required" : "Optional")}) -> {(result.Succeeded ? "Passed" : "Failed")}\nDetail: {check.PreCheckDetails.ReplaceLineEndings(" ")}");
|
||||||
|
|
||||||
if (check.IsRequired)
|
if (check.IsRequired)
|
||||||
{
|
{
|
||||||
requiredResults.Add(result);
|
requiredResults.Add(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requiredResults.Any(result => !result.Succeeded))
|
if (requiredResults.Any(result => !result.Succeeded))
|
||||||
{
|
{
|
||||||
return Result.FromError("Some required checks have failed");
|
return Result.FromError("Some required checks have failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.FromSuccess();
|
return Result.FromSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> RunTasks()
|
public async Task<IResult> RunTasks()
|
||||||
{
|
{
|
||||||
_installRunning = true;
|
_installRunning = true;
|
||||||
|
|
||||||
Log.Information("-<>--<>- Running Installer Tasks -<>--<>-");
|
Log.Information("-<>--<>- Running Installer Tasks -<>--<>-");
|
||||||
|
|
||||||
foreach (var task in _tasks)
|
foreach (var task in _tasks)
|
||||||
{
|
{
|
||||||
TaskChanged?.Invoke(null, task);
|
TaskChanged?.Invoke(null, task);
|
||||||
|
|
||||||
var result = await task.RunAsync();
|
var result = await task.RunAsync();
|
||||||
|
|
||||||
if (!result.Succeeded) return result;
|
if (!result.Succeeded) return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.FromSuccess("Install Complete. Happy Playing!");
|
return Result.FromSuccess("Install Complete. Happy Playing!");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,17 +11,17 @@ public class InvertedProgressConverter : IValueConverter
|
|||||||
{
|
{
|
||||||
return 100 - progress;
|
return 100 - progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
if (value is int invertedProgress)
|
if (value is int invertedProgress)
|
||||||
{
|
{
|
||||||
return 100 - invertedProgress;
|
return 100 - invertedProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
39
SPTInstaller/Converters/StateSpinnerStateToColorConverter.cs
Normal file
39
SPTInstaller/Converters/StateSpinnerStateToColorConverter.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using SPTInstaller.CustomControls;
|
||||||
|
|
||||||
|
namespace SPTInstaller.Converters;
|
||||||
|
|
||||||
|
public class StateSpinnerStateToColorConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (value is not StatusSpinner.SpinnerState state)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case StatusSpinner.SpinnerState.Pending:
|
||||||
|
return new SolidColorBrush(Colors.Gray);
|
||||||
|
case StatusSpinner.SpinnerState.Running:
|
||||||
|
return new SolidColorBrush(Colors.DodgerBlue);
|
||||||
|
case StatusSpinner.SpinnerState.OK:
|
||||||
|
return new SolidColorBrush(Colors.ForestGreen);
|
||||||
|
case StatusSpinner.SpinnerState.Warning:
|
||||||
|
return new SolidColorBrush(Colors.Goldenrod);
|
||||||
|
case StatusSpinner.SpinnerState.Error:
|
||||||
|
return new SolidColorBrush(Colors.Crimson);
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
@ -3,24 +3,25 @@ using SPTInstaller.CustomControls;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace SPTInstaller.Converters;
|
namespace SPTInstaller.Converters;
|
||||||
|
|
||||||
public class StatusSpinnerIsProcessingConverter : IValueConverter
|
public class StatusSpinnerIsProcessingConverter : IValueConverter
|
||||||
{
|
{
|
||||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
if (value is not StatusSpinner.SpinnerState state)
|
if (value is not StatusSpinner.SpinnerState state)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
|
||||||
if (parameter is string parm && parm == "invert")
|
if (parameter is string parm && parm == "invert")
|
||||||
{
|
{
|
||||||
return state > 0;
|
return state > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return state <= 0;
|
return state <= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,24 +3,25 @@ using SPTInstaller.CustomControls;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace SPTInstaller.Converters;
|
namespace SPTInstaller.Converters;
|
||||||
|
|
||||||
public class StatusSpinnerIsStateConverter : IValueConverter
|
public class StatusSpinnerIsStateConverter : IValueConverter
|
||||||
{
|
{
|
||||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
if (value == null || parameter == null)
|
if (value == null || parameter == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (value is not StatusSpinner.SpinnerState state)
|
if (value is not StatusSpinner.SpinnerState state)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (parameter is not string stateName)
|
if (parameter is not string stateName)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return state.ToString().ToLower() == stateName.ToLower();
|
return state.ToString().ToLower() == stateName.ToLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,25 +7,24 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="SPTInstaller.CustomControls.CacheInfo">
|
x:Class="SPTInstaller.CustomControls.CacheInfo">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<convt:StatusSpinnerIsProcessingConverter x:Key="IsInProcessingStateConverter"/>
|
<convt:StatusSpinnerIsProcessingConverter x:Key="IsInProcessingStateConverter" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="AUTO,10,AUTO,10,AUTO">
|
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,AUTO,10,AUTO,*">
|
||||||
<cc:StatusSpinner State="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
<cc:StatusSpinner Grid.Column="1"
|
||||||
|
State="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
IsVisible="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
IsVisible="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
||||||
Converter={StaticResource IsInProcessingStateConverter}}"
|
Converter={StaticResource IsInProcessingStateConverter}}" />
|
||||||
/>
|
|
||||||
|
<Path Grid.Column="1" Data="{StaticResource Cache}" Fill="DodgerBlue" Margin="0 6 0 0"
|
||||||
<Path Data="{StaticResource Cache}" Fill="DodgerBlue" Margin="0 6 0 0"
|
|
||||||
IsVisible="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
IsVisible="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
||||||
Converter={StaticResource IsInProcessingStateConverter},
|
Converter={StaticResource IsInProcessingStateConverter},
|
||||||
ConverterParameter=invert}"
|
ConverterParameter=invert}" />
|
||||||
/>
|
|
||||||
|
<Label Grid.Column="3" Content="{Binding InfoText, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
<Label Grid.Column="2" Content="{Binding InfoText, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Margin="0 2 0 0" />
|
||||||
Margin="0 2 0 0"
|
<Button Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" Content="What's this?" Classes="link"
|
||||||
/>
|
HorizontalAlignment="Center"
|
||||||
<Button Grid.Column="4" Content="What's this?" Classes="link"
|
Command="{Binding ShowCacheDialogCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
Command="{Binding ShowCacheDialogCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -5,30 +5,31 @@ using SPTInstaller.CustomControls.Dialogs;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SPTInstaller.CustomControls;
|
namespace SPTInstaller.CustomControls;
|
||||||
|
|
||||||
public partial class CacheInfo : UserControl
|
public partial class CacheInfo : UserControl
|
||||||
{
|
{
|
||||||
public CacheInfo()
|
public CacheInfo()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ShowCacheDialogCommand() => await DialogHost.Show(new WhyCacheThoughDialog());
|
public async Task ShowCacheDialogCommand() => await DialogHost.Show(new WhyCacheThoughDialog());
|
||||||
|
|
||||||
public string InfoText
|
public string InfoText
|
||||||
{
|
{
|
||||||
get => GetValue(InfoTextProperty);
|
get => GetValue(InfoTextProperty);
|
||||||
set => SetValue(InfoTextProperty, value);
|
set => SetValue(InfoTextProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> InfoTextProperty =
|
public static readonly StyledProperty<string> InfoTextProperty =
|
||||||
AvaloniaProperty.Register<CacheInfo, string>(nameof(InfoText));
|
AvaloniaProperty.Register<CacheInfo, string>(nameof(InfoText));
|
||||||
|
|
||||||
public StatusSpinner.SpinnerState State
|
public StatusSpinner.SpinnerState State
|
||||||
{
|
{
|
||||||
get => GetValue(StateProperty);
|
get => GetValue(StateProperty);
|
||||||
set => SetValue(StateProperty, value);
|
set => SetValue(StateProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<StatusSpinner.SpinnerState> StateProperty =
|
public static readonly StyledProperty<StatusSpinner.SpinnerState> StateProperty =
|
||||||
AvaloniaProperty.Register<CacheInfo, StatusSpinner.SpinnerState>(nameof(State));
|
AvaloniaProperty.Register<CacheInfo, StatusSpinner.SpinnerState>(nameof(State));
|
||||||
}
|
}
|
@ -1,41 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:cc="using:SPTInstaller.CustomControls"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="SPTInstaller.CustomControls.DetailedPreCheckItem"
|
|
||||||
Background="Transparent" MinHeight="100">
|
|
||||||
|
|
||||||
<Grid RowDefinitions="3,AUTO,3,*,30,3" ColumnDefinitions="3,*,AUTO,3" Margin="10">
|
|
||||||
<Border Grid.RowSpan="5" Grid.ColumnSpan="3"
|
|
||||||
Background="{StaticResource AKI_Background_Light}" CornerRadius="8"
|
|
||||||
BoxShadow="3 3 10 .1 black"
|
|
||||||
/>
|
|
||||||
<Border Grid.RowSpan="3" Grid.ColumnSpan="3"
|
|
||||||
Background="{StaticResource AKI_Brush_DarkGrayBlue}" CornerRadius="8 8 0 0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Grid Grid.Row="1" Grid.Column="1" ColumnDefinitions="AUTO, AUTO" Margin="3">
|
|
||||||
<cc:StatusSpinner State="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
|
||||||
<Label Grid.Column="1"
|
|
||||||
Content="{Binding PreCheckName, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
Classes.bold="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
|
||||||
Converter={StaticResource ResourceKey=IsStateConverter},
|
|
||||||
ConverterParameter=Running}"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" TextWrapping="Wrap"
|
|
||||||
Margin="10"
|
|
||||||
Text="{Binding PreCheckDetails, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button Grid.Row="4" Grid.Column="2"
|
|
||||||
CornerRadius="8 0 8 0" Classes="outlinedTLCorner"
|
|
||||||
Content="{Binding ActionButtonText, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
Command="{Binding ActionButtonCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
IsVisible="{Binding ActionButtonIsVisible, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
@ -1,48 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using System.Windows.Input;
|
|
||||||
|
|
||||||
namespace SPTInstaller.CustomControls;
|
|
||||||
public partial class DetailedPreCheckItem : PreCheckItem
|
|
||||||
{
|
|
||||||
public DetailedPreCheckItem()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PreCheckDetails
|
|
||||||
{
|
|
||||||
get => GetValue(PreCheckDetailsProperty);
|
|
||||||
set => SetValue(PreCheckDetailsProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<string> PreCheckDetailsProperty =
|
|
||||||
AvaloniaProperty.Register<DetailedPreCheckItem, string>(nameof(PreCheckDetails));
|
|
||||||
|
|
||||||
public bool ActionButtonIsVisible
|
|
||||||
{
|
|
||||||
get => GetValue(ActionButtonIsVisibleProperty);
|
|
||||||
set => SetValue(ActionButtonIsVisibleProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> ActionButtonIsVisibleProperty =
|
|
||||||
AvaloniaProperty.Register<DetailedPreCheckItem, bool>(nameof(ActionButtonIsVisible));
|
|
||||||
|
|
||||||
public string ActionButtonText
|
|
||||||
{
|
|
||||||
get => GetValue(ActionButtonTextProperty);
|
|
||||||
set => SetValue(ActionButtonTextProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<string> ActionButtonTextProperty =
|
|
||||||
AvaloniaProperty.Register<DetailedPreCheckItem, string>(nameof(ActionButtonText));
|
|
||||||
|
|
||||||
public ICommand ActionButtonCommand
|
|
||||||
{
|
|
||||||
get => GetValue(ActionButtonCommandProperty);
|
|
||||||
set => SetValue(ActionButtonCommandProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<ICommand> ActionButtonCommandProperty =
|
|
||||||
AvaloniaProperty.Register<DetailedPreCheckItem, ICommand>(nameof(ActionButtonCommand));
|
|
||||||
|
|
||||||
}
|
|
@ -5,24 +5,21 @@
|
|||||||
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
|
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
|
||||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300"
|
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300"
|
||||||
x:Class="SPTInstaller.CustomControls.Dialogs.ConfirmationDialog"
|
x:Class="SPTInstaller.CustomControls.Dialogs.ConfirmationDialog"
|
||||||
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" />
|
||||||
/>
|
|
||||||
<Button Content="No" Grid.Row="3" Grid.Column="2"
|
<Button Content="No" Grid.Row="3" Grid.Column="2"
|
||||||
Width="50" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
|
Width="50" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
|
||||||
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}"
|
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}"
|
||||||
CommandParameter="False"
|
CommandParameter="False"
|
||||||
Classes="yellow"
|
Classes="yellow" />
|
||||||
/>
|
|
||||||
<Button Content="Yes" Grid.Row="3" Grid.Column="4"
|
<Button Content="Yes" Grid.Row="3" Grid.Column="4"
|
||||||
Width="50" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
|
Width="50" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
|
||||||
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}"
|
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}"
|
||||||
CommandParameter="True"
|
CommandParameter="True" />
|
||||||
/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -2,21 +2,22 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
|
||||||
namespace SPTInstaller.CustomControls.Dialogs;
|
namespace SPTInstaller.CustomControls.Dialogs;
|
||||||
|
|
||||||
public partial class ConfirmationDialog : UserControl
|
public partial class ConfirmationDialog : UserControl
|
||||||
{
|
{
|
||||||
public ConfirmationDialog(string message)
|
public ConfirmationDialog(string message)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
Message = message;
|
Message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Message
|
public string Message
|
||||||
{
|
{
|
||||||
get => GetValue(MessageProperty);
|
get => GetValue(MessageProperty);
|
||||||
set => SetValue(MessageProperty, value);
|
set => SetValue(MessageProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> MessageProperty =
|
public static readonly StyledProperty<string> MessageProperty =
|
||||||
AvaloniaProperty.Register<ConfirmationDialog, string>(nameof(Message));
|
AvaloniaProperty.Register<ConfirmationDialog, string>(nameof(Message));
|
||||||
}
|
}
|
@ -6,44 +6,43 @@
|
|||||||
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" FontSize="25">♥️</Span>
|
It also helps us prevent extra traffic to our limited download mirrors. Every bit helps <Span Foreground="red"
|
||||||
|
FontSize="25">♥️</Span>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<Label Grid.Row="2" Content="You can find the cache folder here"
|
<Label Grid.Row="2" Content="You can find the cache folder here" />
|
||||||
/>
|
<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"
|
||||||
IsVisible="{Binding !CacheExists, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
IsVisible="{Binding !CacheExists, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
/>
|
|
||||||
|
<Label Grid.Row="4"
|
||||||
|
Content="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=AdditionalInfo}"
|
||||||
|
Foreground="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=AdditionalInfoColor}" />
|
||||||
|
|
||||||
<Label Grid.Row="4"
|
<StackPanel Orientation="Horizontal" Grid.Row="4" Grid.Column="1" Spacing="10">
|
||||||
Content="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=AdditionalInfo}"
|
<Button Content="Move Downloaded Patcher"
|
||||||
Foreground="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=AdditionalInfoColor}"
|
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=MoveDownloadsPatcherToCache}" />
|
||||||
/>
|
|
||||||
|
<Button Content="Clear Metadata Cache"
|
||||||
<Button Grid.Row="4" Grid.Column="1" Content="Move Downloaded Patcher" Margin="0 0 10 0"
|
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ClearCachedMetaData}"
|
||||||
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=MoveDownloadsPatcherToCache}"
|
/>
|
||||||
/>
|
|
||||||
|
<Button Content="Close" Classes="yellow"
|
||||||
<Button Grid.Row="4" Grid.Column="2" Content="Close" Classes="yellow"
|
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}" />
|
||||||
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}"
|
</StackPanel>
|
||||||
/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -3,84 +3,139 @@ 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;
|
||||||
|
|
||||||
public partial class WhyCacheThoughDialog : UserControl
|
public partial class WhyCacheThoughDialog : UserControl
|
||||||
{
|
{
|
||||||
private int _movePatcherState = 0;
|
private int _movePatcherState = 0;
|
||||||
|
|
||||||
private FileInfo? _foundPatcher;
|
private FileInfo? _foundPatcher;
|
||||||
|
|
||||||
public WhyCacheThoughDialog()
|
public WhyCacheThoughDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> AdditionalInfoProperty =
|
public static readonly StyledProperty<string> AdditionalInfoProperty =
|
||||||
AvaloniaProperty.Register<WhyCacheThoughDialog, string>(nameof(AdditionalInfo));
|
AvaloniaProperty.Register<WhyCacheThoughDialog, string>(nameof(AdditionalInfo));
|
||||||
|
|
||||||
public string AdditionalInfo
|
public string AdditionalInfo
|
||||||
{
|
{
|
||||||
get => GetValue(AdditionalInfoProperty);
|
get => GetValue(AdditionalInfoProperty);
|
||||||
set => SetValue(AdditionalInfoProperty, value);
|
set => SetValue(AdditionalInfoProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> AdditionalInfoColorProperty =
|
public static readonly StyledProperty<string> AdditionalInfoColorProperty =
|
||||||
AvaloniaProperty.Register<WhyCacheThoughDialog, string>(nameof(AdditionalInfoColor));
|
AvaloniaProperty.Register<WhyCacheThoughDialog, string>(nameof(AdditionalInfoColor));
|
||||||
|
|
||||||
public string AdditionalInfoColor
|
public string AdditionalInfoColor
|
||||||
{
|
{
|
||||||
get => GetValue(AdditionalInfoColorProperty);
|
get => GetValue(AdditionalInfoColorProperty);
|
||||||
set => SetValue(AdditionalInfoColorProperty, value);
|
set => SetValue(AdditionalInfoColorProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public bool CacheExists => Directory.Exists(DownloadCacheHelper.CachePath);
|
public bool CacheExists => Directory.Exists(DownloadCacheHelper.CachePath);
|
||||||
|
|
||||||
public void OpenCacheFolder()
|
public void OpenCacheFolder()
|
||||||
{
|
{
|
||||||
if (!CacheExists)
|
if (!CacheExists)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Process.Start(new ProcessStartInfo()
|
Process.Start(new ProcessStartInfo()
|
||||||
{
|
{
|
||||||
FileName = Path.EndsInDirectorySeparator(DownloadCacheHelper.CachePath) ? DownloadCacheHelper.CachePath : DownloadCacheHelper.CachePath + Path.DirectorySeparatorChar,
|
FileName = Path.EndsInDirectorySeparator(DownloadCacheHelper.CachePath)
|
||||||
|
? DownloadCacheHelper.CachePath
|
||||||
|
: DownloadCacheHelper.CachePath + Path.DirectorySeparatorChar,
|
||||||
UseShellExecute = true,
|
UseShellExecute = true,
|
||||||
Verb = "open"
|
Verb = "open"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
if (!downloadsFolder.Exists)
|
if (!downloadsFolder.Exists)
|
||||||
{
|
{
|
||||||
AdditionalInfo = "Could not get downloads folder :(";
|
var message = "Could not get downloads folder :(";
|
||||||
|
Log.Error($"[MV_0] {message}");
|
||||||
|
AdditionalInfo = message;
|
||||||
AdditionalInfoColor = "red";
|
AdditionalInfoColor = "red";
|
||||||
_movePatcherState = -1;
|
_movePatcherState = -1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_foundPatcher = downloadsFolder.GetFiles("Patcher_*").OrderByDescending(p => p.CreationTime).FirstOrDefault();
|
_foundPatcher = downloadsFolder.GetFiles("Patcher_*").OrderByDescending(p => p.CreationTime)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (_foundPatcher == null || !_foundPatcher.Exists)
|
if (_foundPatcher == null || !_foundPatcher.Exists)
|
||||||
{
|
{
|
||||||
AdditionalInfo =
|
var message = "Could not find a patcher file in your downloads folder";
|
||||||
"Could not find a patcher file in your downloads folder";
|
Log.Warning($"[MV_0] {message}");
|
||||||
|
AdditionalInfo = message;
|
||||||
|
|
||||||
AdditionalInfoColor = "red";
|
AdditionalInfoColor = "red";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AdditionalInfo = $"Click again to move the below patcher file to the cache folder\n{_foundPatcher?.Name ?? "-SOMETHING WENT WRONG-"}";
|
Log.Information($"[MV_0] Found patcher for move: {_foundPatcher.Name}");
|
||||||
|
AdditionalInfo =
|
||||||
|
$"Click again to move the below patcher file to the cache folder\n{_foundPatcher?.Name ?? "-SOMETHING WENT WRONG-"}";
|
||||||
AdditionalInfoColor = "#FFC107";
|
AdditionalInfoColor = "#FFC107";
|
||||||
_movePatcherState = 1;
|
_movePatcherState = 1;
|
||||||
break;
|
break;
|
||||||
@ -89,7 +144,9 @@ public partial class WhyCacheThoughDialog : UserControl
|
|||||||
{
|
{
|
||||||
var cacheFilePath = Path.Join(DownloadCacheHelper.CachePath, "patcher");
|
var cacheFilePath = Path.Join(DownloadCacheHelper.CachePath, "patcher");
|
||||||
_foundPatcher?.MoveTo(cacheFilePath, true);
|
_foundPatcher?.MoveTo(cacheFilePath, true);
|
||||||
AdditionalInfo = "Patch was moved into cache :D";
|
var message = "Patcher was moved into cache :D";
|
||||||
|
Log.Information($"[MV_1] {message}");
|
||||||
|
AdditionalInfo = message;
|
||||||
AdditionalInfoColor = "ForestGreen";
|
AdditionalInfoColor = "ForestGreen";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -98,7 +155,11 @@ public partial class WhyCacheThoughDialog : UserControl
|
|||||||
AdditionalInfoColor = "red";
|
AdditionalInfoColor = "red";
|
||||||
Log.Error(ex, "Failed to move downloaded patcher file into cache");
|
Log.Error(ex, "Failed to move downloaded patcher file into cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.Error("[MV_ ] Move state is broken :(");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,66 +10,67 @@ public class DistributedSpacePanel : Panel
|
|||||||
protected override Size MeasureOverride(Size availableSize)
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
{
|
{
|
||||||
var children = Children;
|
var children = Children;
|
||||||
|
|
||||||
for (int i = 0; i < children.Count; i++)
|
for (int i = 0; i < children.Count; i++)
|
||||||
{
|
{
|
||||||
// measure child objects so we can use their desired size in the arrange override
|
// measure child objects so we can use their desired size in the arrange override
|
||||||
var child = children[i];
|
var child = children[i];
|
||||||
child.Measure(availableSize);
|
child.Measure(availableSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// we want to use all available space
|
// we want to use all available space
|
||||||
return availableSize;
|
return availableSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Size ArrangeOverride(Size finalSize)
|
protected override Size ArrangeOverride(Size finalSize)
|
||||||
{
|
{
|
||||||
var children = Children;
|
var children = Children;
|
||||||
Rect rcChild = new Rect(finalSize);
|
Rect rcChild = new Rect(finalSize);
|
||||||
double previousChildSize = 0.0;
|
double previousChildSize = 0.0;
|
||||||
|
|
||||||
// get child objects that don't want to span the entire control
|
// get child objects that don't want to span the entire control
|
||||||
var nonSpanningChildren = children.Where(x => x.GetValue(SpanBehavior.SpanProperty) == false).ToList();
|
var nonSpanningChildren = children.Where(x => x.GetValue(SpanBehavior.SpanProperty) == false).ToList();
|
||||||
|
|
||||||
// get the total height off all non-spanning child objects
|
// get the total height off all non-spanning child objects
|
||||||
var totalChildHeight = nonSpanningChildren.Select(x => x.DesiredSize.Height).Sum();
|
var totalChildHeight = nonSpanningChildren.Select(x => x.DesiredSize.Height).Sum();
|
||||||
|
|
||||||
// remove the total child height from our controls final size and divide it by the total non-spanning child objects
|
// remove the total child height from our controls final size and divide it by the total non-spanning child objects
|
||||||
// except the last one, since it needs no space after it
|
// except the last one, since it needs no space after it
|
||||||
var spacing = (finalSize.Height - totalChildHeight) / (nonSpanningChildren.Count - 1);
|
var spacing = (finalSize.Height - totalChildHeight) / (nonSpanningChildren.Count - 1);
|
||||||
|
|
||||||
for (int i = 0; i < children.Count; i++)
|
for (int i = 0; i < children.Count; i++)
|
||||||
{
|
{
|
||||||
var child = children[i];
|
var child = children[i];
|
||||||
|
|
||||||
var spanChild = child.GetValue(SpanBehavior.SpanProperty);
|
var spanChild = child.GetValue(SpanBehavior.SpanProperty);
|
||||||
|
|
||||||
if (spanChild)
|
if (spanChild)
|
||||||
{
|
{
|
||||||
// move any spanning children to the top of the array to push them behind the other controls (visually)
|
// move any spanning children to the top of the array to push them behind the other controls (visually)
|
||||||
children.Move(i, 0);
|
children.Move(i, 0);
|
||||||
|
|
||||||
rcChild = rcChild.WithY(0)
|
rcChild = rcChild.WithY(0)
|
||||||
.WithX(0)
|
.WithX(0)
|
||||||
.WithHeight(finalSize.Height)
|
.WithHeight(finalSize.Height)
|
||||||
.WithWidth(finalSize.Width);
|
.WithWidth(finalSize.Width);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
child.Arrange(rcChild);
|
child.Arrange(rcChild);
|
||||||
continue;
|
continue;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
|
rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
|
||||||
previousChildSize = child.DesiredSize.Height;
|
previousChildSize = child.DesiredSize.Height;
|
||||||
rcChild = rcChild.WithHeight(previousChildSize)
|
rcChild = rcChild.WithHeight(previousChildSize)
|
||||||
.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
|
.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
|
||||||
|
|
||||||
previousChildSize += spacing;
|
previousChildSize += spacing;
|
||||||
|
|
||||||
child.Arrange(rcChild);
|
child.Arrange(rcChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
return finalSize;
|
return finalSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
58
SPTInstaller/CustomControls/PreCheckDetails.axaml
Normal file
58
SPTInstaller/CustomControls/PreCheckDetails.axaml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<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:model="using:SPTInstaller.Models"
|
||||||
|
xmlns:cvt="using:SPTInstaller.Converters"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="SPTInstaller.CustomControls.PreCheckDetails">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<cvt:StateSpinnerStateToColorConverter x:Key="colorConverter" />
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<!-- show when nothing is selected -->
|
||||||
|
<Label Content="Select a Pre-Check to see more info" FontSize="20"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding HasSelection, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={x:Static BoolConverters.Not}}" />
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding PreChecks}" VerticalAlignment="Stretch">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<Grid />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="model:PreCheckBase">
|
||||||
|
<!-- selected precheck details -->
|
||||||
|
<Grid RowDefinitions="10, *, Auto, 10" ColumnDefinitions="10, 10, *, 10"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
IsVisible="{Binding IsSelected}">
|
||||||
|
<Rectangle Grid.Row="1" Grid.Column="1" Width="3"
|
||||||
|
Fill="{Binding State, Converter={StaticResource colorConverter}}"
|
||||||
|
HorizontalAlignment="Left" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Grid.Column="2" HorizontalAlignment="Left">
|
||||||
|
<Label Content="{Binding Name}"
|
||||||
|
FontSize="20" />
|
||||||
|
<Rectangle Height="1" Fill="Gray" Margin="0 10" />
|
||||||
|
<TextBlock Text="{Binding PreCheckDetails}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Classes="yellow"
|
||||||
|
IsVisible="{Binding ActionButtonIsVisible}"
|
||||||
|
CornerRadius="15"
|
||||||
|
Margin="0 10"
|
||||||
|
Command="{Binding ActionButtonCommand}"
|
||||||
|
Content="{Binding ActionButtonText}"
|
||||||
|
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
|
||||||
|
HorizontalAlignment="Stretch" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Panel>
|
||||||
|
</UserControl>
|
32
SPTInstaller/CustomControls/PreCheckDetails.axaml.cs
Normal file
32
SPTInstaller/CustomControls/PreCheckDetails.axaml.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using SPTInstaller.Models;
|
||||||
|
|
||||||
|
namespace SPTInstaller.CustomControls;
|
||||||
|
|
||||||
|
public partial class PreCheckDetails : UserControl
|
||||||
|
{
|
||||||
|
public PreCheckDetails()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<ObservableCollection<PreCheckBase>> PreChecksProperty =
|
||||||
|
AvaloniaProperty.Register<PreCheckDetails, ObservableCollection<PreCheckBase>>(nameof(PreChecks));
|
||||||
|
|
||||||
|
public ObservableCollection<PreCheckBase> PreChecks
|
||||||
|
{
|
||||||
|
get => GetValue(PreChecksProperty);
|
||||||
|
set => SetValue(PreChecksProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> HasSelectionProperty =
|
||||||
|
AvaloniaProperty.Register<PreCheckDetails, bool>(nameof(HasSelection));
|
||||||
|
|
||||||
|
public bool HasSelection
|
||||||
|
{
|
||||||
|
get => GetValue(HasSelectionProperty);
|
||||||
|
set => SetValue(HasSelectionProperty, value);
|
||||||
|
}
|
||||||
|
}
|
@ -8,23 +8,53 @@
|
|||||||
x:Class="SPTInstaller.CustomControls.PreCheckItem">
|
x:Class="SPTInstaller.CustomControls.PreCheckItem">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<convt:StatusSpinnerIsStateConverter x:Key="IsStateConverter"/>
|
<convt:StatusSpinnerIsStateConverter x:Key="IsStateConverter" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector="Label.bold">
|
<Style Selector="Label.bold">
|
||||||
<Setter Property="FontWeight" Value="Bold"/>
|
<Setter Property="FontWeight" Value="Bold" />
|
||||||
</Style>
|
</Style>
|
||||||
</UserControl.Styles>
|
|
||||||
|
<Style Selector="Button.selectable">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Grid ColumnDefinitions="AUTO, AUTO" Margin="3">
|
<Setter Property="Foreground" Value="{StaticResource SPT_Background_Dark}" />
|
||||||
<cc:StatusSpinner State="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</Style>
|
||||||
<Label Grid.Column="1"
|
|
||||||
Content="{Binding PreCheckName, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
<Style Selector="Button.selectable:pointerover">
|
||||||
Classes.bold="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.selectable:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.selectable:disabled /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.selected">
|
||||||
|
<Setter Property="Background" Value="{StaticResource SPT_Brush_LightGrayBlue}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<Button CornerRadius="15" Padding="0" Margin="0" HorizontalAlignment="Stretch" VerticalContentAlignment="Center"
|
||||||
|
Command="{Binding SelectCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding }"
|
||||||
|
Classes.selectable="{Binding !IsSelected, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
Classes.selected="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=UserControl}}">
|
||||||
|
<Grid ColumnDefinitions="AUTO, AUTO" Margin="3 0 0 3">
|
||||||
|
<cc:StatusSpinner State="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
|
|
||||||
|
<Label Grid.Column="1"
|
||||||
|
Content="{Binding PreCheckName, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
Classes.bold="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
||||||
Converter={StaticResource ResourceKey=IsStateConverter},
|
Converter={StaticResource ResourceKey=IsStateConverter},
|
||||||
ConverterParameter=Running}"
|
ConverterParameter=Running}" />
|
||||||
/>
|
</Grid>
|
||||||
</Grid>
|
</Button>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,4 +1,5 @@
|
|||||||
using Avalonia;
|
using System.Windows.Input;
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
|
||||||
namespace SPTInstaller.CustomControls;
|
namespace SPTInstaller.CustomControls;
|
||||||
@ -9,31 +10,50 @@ public partial class PreCheckItem : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string PreCheckName
|
public string PreCheckName
|
||||||
{
|
{
|
||||||
get => GetValue(PreCheckNameProperty);
|
get => GetValue(PreCheckNameProperty);
|
||||||
set => SetValue(PreCheckNameProperty, value);
|
set => SetValue(PreCheckNameProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> PreCheckNameProperty =
|
public static readonly StyledProperty<string> PreCheckNameProperty =
|
||||||
AvaloniaProperty.Register<PreCheckItem, string>(nameof(PreCheckName));
|
AvaloniaProperty.Register<PreCheckItem, string>(nameof(PreCheckName));
|
||||||
|
|
||||||
public bool IsRequired
|
public bool IsRequired
|
||||||
{
|
{
|
||||||
get => GetValue(IsRequiredProperty);
|
get => GetValue(IsRequiredProperty);
|
||||||
set => SetValue(IsRequiredProperty, value);
|
set => SetValue(IsRequiredProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> IsRequiredProperty =
|
public static readonly StyledProperty<bool> IsRequiredProperty =
|
||||||
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsRequired));
|
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsRequired));
|
||||||
|
|
||||||
public StatusSpinner.SpinnerState State
|
public StatusSpinner.SpinnerState State
|
||||||
{
|
{
|
||||||
get => GetValue(StateProperty);
|
get => GetValue(StateProperty);
|
||||||
set => SetValue(StateProperty, value);
|
set => SetValue(StateProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<StatusSpinner.SpinnerState> StateProperty =
|
public static readonly StyledProperty<StatusSpinner.SpinnerState> StateProperty =
|
||||||
AvaloniaProperty.Register<PreCheckItem, StatusSpinner.SpinnerState>(nameof(State));
|
AvaloniaProperty.Register<PreCheckItem, StatusSpinner.SpinnerState>(nameof(State));
|
||||||
|
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> IsSelectedProperty =
|
||||||
|
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsSelected));
|
||||||
|
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => GetValue(IsSelectedProperty);
|
||||||
|
set => SetValue(IsSelectedProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<ICommand> SelectCommandProperty =
|
||||||
|
AvaloniaProperty.Register<PreCheckItem, ICommand>(nameof(SelectCommand));
|
||||||
|
|
||||||
|
public ICommand SelectCommand
|
||||||
|
{
|
||||||
|
get => GetValue(SelectCommandProperty);
|
||||||
|
set => SetValue(SelectCommandProperty, value);
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,78 +2,83 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="using:SPTInstaller.ViewModels"
|
xmlns:vm="using:SPTInstaller.ViewModels"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="SPTInstaller.CustomControls.ProgressableTaskItem">
|
x:Class="SPTInstaller.CustomControls.ProgressableTaskItem">
|
||||||
|
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<!-- Ellipse Styles -->
|
<!-- Ellipse Styles -->
|
||||||
<Style Selector="Ellipse">
|
<Style Selector="Ellipse">
|
||||||
<Setter Property="Stroke" Value="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
<Setter Property="Stroke"
|
||||||
<Setter Property="Margin" Value="7 0"/>
|
Value="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
</Style>
|
<Setter Property="Margin" Value="7 0" />
|
||||||
<Style Selector="Ellipse.completed">
|
</Style>
|
||||||
<Setter Property="Stroke" Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
<Style Selector="Ellipse.completed">
|
||||||
</Style>
|
<Setter Property="Stroke"
|
||||||
<Style Selector="Ellipse.running">
|
Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
<Setter Property="Stroke" Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
</Style>
|
||||||
<Setter Property="Margin" Value="7 3"/>
|
<Style Selector="Ellipse.running">
|
||||||
<Style.Animations>
|
<Setter Property="Stroke"
|
||||||
<Animation Duration="0:0:1" PlaybackDirection="Alternate" IterationCount="Infinite">
|
Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
<KeyFrame Cue="0%">
|
<Setter Property="Margin" Value="7 3" />
|
||||||
<Setter Property="ScaleTransform.ScaleX" Value="1"/>
|
<Style.Animations>
|
||||||
<Setter Property="ScaleTransform.ScaleY" Value="1"/>
|
<Animation Duration="0:0:1" PlaybackDirection="Alternate" IterationCount="Infinite">
|
||||||
</KeyFrame>
|
<KeyFrame Cue="0%">
|
||||||
<KeyFrame Cue="100%">
|
<Setter Property="ScaleTransform.ScaleX" Value="1" />
|
||||||
<Setter Property="ScaleTransform.ScaleX" Value="1.2"/>
|
<Setter Property="ScaleTransform.ScaleY" Value="1" />
|
||||||
<Setter Property="ScaleTransform.ScaleY" Value="1.2"/>
|
</KeyFrame>
|
||||||
</KeyFrame>
|
<KeyFrame Cue="100%">
|
||||||
</Animation>
|
<Setter Property="ScaleTransform.ScaleX" Value="1.2" />
|
||||||
</Style.Animations>
|
<Setter Property="ScaleTransform.ScaleY" Value="1.2" />
|
||||||
</Style>
|
</KeyFrame>
|
||||||
<Style Selector="Ellipse.centerRunning">
|
</Animation>
|
||||||
<Setter Property="Fill" Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
</Style.Animations>
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Ellipse.centerCompleted">
|
<Style Selector="Ellipse.centerRunning">
|
||||||
<Setter Property="Fill" Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
<Setter Property="Fill"
|
||||||
</Style>
|
Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Ellipse.centerCompleted">
|
||||||
|
<Setter Property="Fill"
|
||||||
|
Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- Label Styles -->
|
<!-- Label Styles -->
|
||||||
<Style Selector="TextBlock">
|
<Style Selector="TextBlock">
|
||||||
<Setter Property="Foreground" Value="{Binding PendingColor, RelativeSource={RelativeSource
|
<Setter Property="Foreground"
|
||||||
AncestorType=UserControl}}"/>
|
Value="{Binding PendingColor, RelativeSource={RelativeSource
|
||||||
</Style>
|
AncestorType=UserControl}}" />
|
||||||
<Style Selector="TextBlock.completed">
|
</Style>
|
||||||
<Setter Property="Foreground" Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
<Style Selector="TextBlock.completed">
|
||||||
</Style>
|
<Setter Property="Foreground"
|
||||||
<Style Selector="TextBlock.running">
|
Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
<Setter Property="Foreground" Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
|
</Style>
|
||||||
</Style>
|
<Style Selector="TextBlock.running">
|
||||||
</UserControl.Styles>
|
<Setter Property="Foreground"
|
||||||
<Grid ColumnDefinitions="AUTO, *">
|
Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
|
</Style>
|
||||||
<Ellipse Height="30" Width="30"
|
</UserControl.Styles>
|
||||||
StrokeThickness="4"
|
<Grid ColumnDefinitions="AUTO, *">
|
||||||
Fill="{StaticResource AKI_Background_Dark}"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
Classes.completed="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Ellipse Height="20" Width="20"
|
<Ellipse Height="30" Width="30"
|
||||||
Classes.centerRunning="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
StrokeThickness="4"
|
||||||
Classes.centerCompleted="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Fill="{StaticResource SPT_Background_Dark}"
|
||||||
/>
|
HorizontalAlignment="Left"
|
||||||
|
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
<TextBlock Grid.Column="1"
|
Classes.completed="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
Text="{Binding TaskName, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
<Ellipse Height="20" Width="20"
|
||||||
Classes.completed="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Classes.centerRunning="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
FontWeight="SemiBold"
|
Classes.centerCompleted="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
FontSize="15"
|
|
||||||
TextWrapping="Wrap"
|
<TextBlock Grid.Column="1"
|
||||||
VerticalAlignment="Center"
|
Text="{Binding TaskName, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
HorizontalAlignment="Left"
|
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
/>
|
Classes.completed="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
</Grid>
|
FontWeight="SemiBold"
|
||||||
</UserControl>
|
FontSize="15"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
@ -10,67 +10,67 @@ public partial class ProgressableTaskItem : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string TaskId
|
public string TaskId
|
||||||
{
|
{
|
||||||
get => GetValue(TaskIdProperty);
|
get => GetValue(TaskIdProperty);
|
||||||
set => SetValue(TaskIdProperty, value);
|
set => SetValue(TaskIdProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> TaskIdProperty =
|
public static readonly StyledProperty<string> TaskIdProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskId));
|
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskId));
|
||||||
|
|
||||||
public string TaskName
|
public string TaskName
|
||||||
{
|
{
|
||||||
get => GetValue(TaskNameProperty);
|
get => GetValue(TaskNameProperty);
|
||||||
set => SetValue(TaskNameProperty, value);
|
set => SetValue(TaskNameProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> TaskNameProperty =
|
public static readonly StyledProperty<string> TaskNameProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskName));
|
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskName));
|
||||||
|
|
||||||
public bool IsCompleted
|
public bool IsCompleted
|
||||||
{
|
{
|
||||||
get => GetValue(IsCompletedProperty);
|
get => GetValue(IsCompletedProperty);
|
||||||
set => SetValue(IsCompletedProperty, value);
|
set => SetValue(IsCompletedProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> IsCompletedProperty =
|
public static readonly StyledProperty<bool> IsCompletedProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsCompleted));
|
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsCompleted));
|
||||||
|
|
||||||
public bool IsRunning
|
public bool IsRunning
|
||||||
{
|
{
|
||||||
get => GetValue(IsRunningProperty);
|
get => GetValue(IsRunningProperty);
|
||||||
set => SetValue(IsRunningProperty, value);
|
set => SetValue(IsRunningProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> IsRunningProperty =
|
public static readonly StyledProperty<bool> IsRunningProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsRunning));
|
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsRunning));
|
||||||
|
|
||||||
public IBrush PendingColor
|
public IBrush PendingColor
|
||||||
{
|
{
|
||||||
get => GetValue(PendingColorProperty);
|
get => GetValue(PendingColorProperty);
|
||||||
set => SetValue(PendingColorProperty, value);
|
set => SetValue(PendingColorProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> PendingColorProperty =
|
public static readonly StyledProperty<IBrush> PendingColorProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
|
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
|
||||||
|
|
||||||
public IBrush RunningColor
|
public IBrush RunningColor
|
||||||
{
|
{
|
||||||
get => GetValue(RunningColorProperty);
|
get => GetValue(RunningColorProperty);
|
||||||
set => SetValue(RunningColorProperty, value);
|
set => SetValue(RunningColorProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> RunningColorProperty =
|
public static readonly StyledProperty<IBrush> RunningColorProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
|
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
|
||||||
|
|
||||||
public IBrush CompletedColor
|
public IBrush CompletedColor
|
||||||
{
|
{
|
||||||
get => GetValue(CompletedColorProperty);
|
get => GetValue(CompletedColorProperty);
|
||||||
set => SetValue(CompletedColorProperty, value);
|
set => SetValue(CompletedColorProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> CompletedColorProperty =
|
public static readonly StyledProperty<IBrush> CompletedColorProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
|
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
|
||||||
}
|
}
|
@ -2,15 +2,14 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:cc="using:SPTInstaller.CustomControls"
|
xmlns:cc="using:SPTInstaller.CustomControls"
|
||||||
xmlns:bh="using:SPTInstaller.Behaviors"
|
xmlns:bh="using:SPTInstaller.Behaviors"
|
||||||
xmlns:convt="using:SPTInstaller.Converters"
|
xmlns:convt="using:SPTInstaller.Converters"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="SPTInstaller.CustomControls.ProgressableTaskList"
|
x:Class="SPTInstaller.CustomControls.ProgressableTaskList">
|
||||||
>
|
<UserControl.Resources>
|
||||||
<UserControl.Resources>
|
<convt:InvertedProgressConverter x:Key="invtProgressConvt" />
|
||||||
<convt:InvertedProgressConverter x:Key="invtProgressConvt"/>
|
</UserControl.Resources>
|
||||||
</UserControl.Resources>
|
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<ProgressBar Orientation="Vertical"
|
<ProgressBar Orientation="Vertical"
|
||||||
@ -19,13 +18,13 @@
|
|||||||
Foreground="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Foreground="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
Margin="25 20"
|
Margin="25 20" />
|
||||||
/>
|
<ItemsControl Name="itemsControl"
|
||||||
<ItemsControl Name="itemsControl" ItemsSource="{Binding Tasks, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
ItemsSource="{Binding Tasks, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
Padding="5">
|
Padding="5">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<cc:DistributedSpacePanel/>
|
<cc:DistributedSpacePanel />
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
@ -36,10 +35,9 @@
|
|||||||
IsCompleted="{Binding IsCompleted}"
|
IsCompleted="{Binding IsCompleted}"
|
||||||
PendingColor="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
PendingColor="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
RunningColor="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
RunningColor="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
CompletedColor="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
CompletedColor="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
/>
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -15,76 +15,77 @@ public partial class ProgressableTaskList : UserControl
|
|||||||
public ProgressableTaskList()
|
public ProgressableTaskList()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
this.AttachedToVisualTree += ProgressableTaskList_AttachedToVisualTree;
|
this.AttachedToVisualTree += ProgressableTaskList_AttachedToVisualTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int _taskProgress;
|
private int _taskProgress;
|
||||||
|
|
||||||
public int TaskProgress
|
public int TaskProgress
|
||||||
{
|
{
|
||||||
get => _taskProgress;
|
get => _taskProgress;
|
||||||
set => SetAndRaise(ProgressableTaskList.TaskProgressProperty, ref _taskProgress, value);
|
set => SetAndRaise(ProgressableTaskList.TaskProgressProperty, ref _taskProgress, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly DirectProperty<ProgressableTaskList, int> TaskProgressProperty =
|
public static readonly DirectProperty<ProgressableTaskList, int> TaskProgressProperty =
|
||||||
AvaloniaProperty.RegisterDirect<ProgressableTaskList, int>(nameof(TaskProgress), o => o.TaskProgress);
|
AvaloniaProperty.RegisterDirect<ProgressableTaskList, int>(nameof(TaskProgress), o => o.TaskProgress);
|
||||||
|
|
||||||
public ObservableCollection<InstallerTaskBase> Tasks
|
public ObservableCollection<InstallerTaskBase> Tasks
|
||||||
{
|
{
|
||||||
get => GetValue(TasksProperty);
|
get => GetValue(TasksProperty);
|
||||||
set => SetValue(TasksProperty, value);
|
set => SetValue(TasksProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<ObservableCollection<InstallerTaskBase>> TasksProperty =
|
public static readonly StyledProperty<ObservableCollection<InstallerTaskBase>> TasksProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskList, ObservableCollection<InstallerTaskBase>>(nameof(Tasks));
|
AvaloniaProperty.Register<ProgressableTaskList, ObservableCollection<InstallerTaskBase>>(nameof(Tasks));
|
||||||
|
|
||||||
public IBrush PendingColor
|
public IBrush PendingColor
|
||||||
{
|
{
|
||||||
get => GetValue(PendingColorProperty);
|
get => GetValue(PendingColorProperty);
|
||||||
set => SetValue(PendingColorProperty, value);
|
set => SetValue(PendingColorProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> PendingColorProperty =
|
public static readonly StyledProperty<IBrush> PendingColorProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
|
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
|
||||||
|
|
||||||
public IBrush RunningColor
|
public IBrush RunningColor
|
||||||
{
|
{
|
||||||
get => GetValue(RunningColorProperty);
|
get => GetValue(RunningColorProperty);
|
||||||
set => SetValue(RunningColorProperty, value);
|
set => SetValue(RunningColorProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> RunningColorProperty =
|
public static readonly StyledProperty<IBrush> RunningColorProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
|
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
|
||||||
|
|
||||||
public IBrush CompletedColor
|
public IBrush CompletedColor
|
||||||
{
|
{
|
||||||
get => GetValue(CompletedColorProperty);
|
get => GetValue(CompletedColorProperty);
|
||||||
set => SetValue(CompletedColorProperty, value);
|
set => SetValue(CompletedColorProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> CompletedColorProperty =
|
public static readonly StyledProperty<IBrush> CompletedColorProperty =
|
||||||
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
|
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
|
||||||
|
|
||||||
private void UpdateTaskProgress()
|
private void UpdateTaskProgress()
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
var completedTasks = Tasks.Where(x => x.IsCompleted == true).Count();
|
var completedTasks = Tasks.Where(x => x.IsCompleted == true).Count();
|
||||||
|
|
||||||
var progress = (int)Math.Floor((double)completedTasks / (Tasks.Count - 1) * 100);
|
var progress = (int)Math.Floor((double)completedTasks / (Tasks.Count - 1) * 100);
|
||||||
|
|
||||||
for(; TaskProgress < progress;)
|
for (; TaskProgress < progress;)
|
||||||
{
|
{
|
||||||
TaskProgress += 1;
|
TaskProgress += 1;
|
||||||
await Task.Delay(1);
|
await Task.Delay(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProgressableTaskList_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
private void ProgressableTaskList_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
if (Tasks == null) return;
|
if (Tasks == null) return;
|
||||||
|
|
||||||
foreach (var task in Tasks)
|
foreach (var task in Tasks)
|
||||||
{
|
{
|
||||||
task.WhenPropertyChanged(x => x.IsCompleted)
|
task.WhenPropertyChanged(x => x.IsCompleted)
|
||||||
|
@ -4,56 +4,55 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:convt="using:SPTInstaller.Converters"
|
xmlns:convt="using:SPTInstaller.Converters"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="SPTInstaller.CustomControls.StatusSpinner"
|
x:Class="SPTInstaller.CustomControls.StatusSpinner">
|
||||||
>
|
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<convt:StatusSpinnerIsStateConverter x:Key="IsStateConverter"/>
|
<convt:StatusSpinnerIsStateConverter x:Key="IsStateConverter" />
|
||||||
<convt:StatusSpinnerIsProcessingConverter x:Key="IsInProcessingStateConverter"/>
|
<convt:StatusSpinnerIsProcessingConverter x:Key="IsInProcessingStateConverter" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector="Arc.running">
|
<Style Selector="Arc.running">
|
||||||
<Setter Property="Stroke" Value="DodgerBlue"/>
|
<Setter Property="Stroke" Value="DodgerBlue" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Path.ok">
|
<Style Selector="Path.ok">
|
||||||
<Setter Property="Data" Value="{StaticResource CircledCheck}"/>
|
<Setter Property="Data" Value="{StaticResource CircledCheck}" />
|
||||||
<Setter Property="Fill" Value="Green"/>
|
<Setter Property="Fill" Value="Green" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Path.warning">
|
<Style Selector="Path.warning">
|
||||||
<Setter Property="Data" Value="{StaticResource CircledWarn}"/>
|
<Setter Property="Data" Value="{StaticResource CircledWarn}" />
|
||||||
<Setter Property="Fill" Value="Goldenrod"/>
|
<Setter Property="Fill" Value="Goldenrod" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Path.error">
|
<Style Selector="Path.error">
|
||||||
<Setter Property="Data" Value="{StaticResource CircledX}"/>
|
<Setter Property="Data" Value="{StaticResource CircledX}" />
|
||||||
<Setter Property="Fill" Value="Red"/>
|
<Setter Property="Fill" Value="Red" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Arc">
|
<Style Selector="Arc">
|
||||||
<Setter Property="Stroke" Value="Gray"/>
|
<Setter Property="Stroke" Value="Gray" />
|
||||||
<Setter Property="IsVisible" Value="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
<Setter Property="IsVisible"
|
||||||
Converter={StaticResource ResourceKey=IsInProcessingStateConverter}}"/>
|
Value="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
||||||
|
Converter={StaticResource ResourceKey=IsInProcessingStateConverter}}" />
|
||||||
<Style.Animations>
|
<Style.Animations>
|
||||||
<Animation Duration="0:0:1" IterationCount="Infinite">
|
<Animation Duration="0:0:1" IterationCount="Infinite">
|
||||||
<KeyFrame Cue="0%">
|
<KeyFrame Cue="0%">
|
||||||
<Setter Property="RotateTransform.Angle" Value="0"/>
|
<Setter Property="RotateTransform.Angle" Value="0" />
|
||||||
</KeyFrame>
|
</KeyFrame>
|
||||||
<KeyFrame Cue="100%">
|
<KeyFrame Cue="100%">
|
||||||
<Setter Property="RotateTransform.Angle" Value="360"/>
|
<Setter Property="RotateTransform.Angle" Value="360" />
|
||||||
</KeyFrame>
|
</KeyFrame>
|
||||||
</Animation>
|
</Animation>
|
||||||
</Style.Animations>
|
</Style.Animations>
|
||||||
</Style>
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Canvas Margin="0 3 0 0" Height="20" Width="20"
|
<Canvas Margin="0 3 0 0" Height="20" Width="20"
|
||||||
IsVisible="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
IsVisible="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
||||||
Converter={StaticResource ResourceKey=IsInProcessingStateConverter}, ConverterParameter=invert}">
|
Converter={StaticResource ResourceKey=IsInProcessingStateConverter}, ConverterParameter=invert}">
|
||||||
<Ellipse Fill="White" Height="15" Width="15" Canvas.Top="3" Canvas.Left="3"
|
<Ellipse Fill="White" Height="15" Width="15" Canvas.Top="3" Canvas.Left="3" />
|
||||||
/>
|
|
||||||
<Path StrokeThickness="2"
|
<Path StrokeThickness="2"
|
||||||
Classes.ok="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
Classes.ok="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
||||||
Converter={StaticResource ResourceKey=IsStateConverter},
|
Converter={StaticResource ResourceKey=IsStateConverter},
|
||||||
@ -63,15 +62,13 @@
|
|||||||
ConverterParameter=Warning}"
|
ConverterParameter=Warning}"
|
||||||
Classes.error="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
Classes.error="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
||||||
Converter={StaticResource ResourceKey=IsStateConverter},
|
Converter={StaticResource ResourceKey=IsStateConverter},
|
||||||
ConverterParameter=Error}"
|
ConverterParameter=Error}" />
|
||||||
/>
|
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
<Arc StartAngle="280" SweepAngle="80" Margin="0 3 0 0" StrokeThickness="3"
|
<Arc StartAngle="280" SweepAngle="80" Margin="0 3 0 0" StrokeThickness="3"
|
||||||
Width="20" Height="20" VerticalAlignment="Top"
|
Width="20" Height="20" VerticalAlignment="Top"
|
||||||
Classes.running="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
Classes.running="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
|
||||||
Converter={StaticResource ResourceKey=IsStateConverter},
|
Converter={StaticResource ResourceKey=IsStateConverter},
|
||||||
ConverterParameter=Running}"
|
ConverterParameter=Running}" />
|
||||||
/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -10,22 +10,22 @@ public partial class StatusSpinner : ReactiveUserControl<UserControl>
|
|||||||
{
|
{
|
||||||
Pending = -1,
|
Pending = -1,
|
||||||
Running = 0,
|
Running = 0,
|
||||||
OK = 1,
|
OK = 1,
|
||||||
Warning = 2,
|
Warning = 2,
|
||||||
Error = 3,
|
Error = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
public StatusSpinner()
|
public StatusSpinner()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpinnerState State
|
public SpinnerState State
|
||||||
{
|
{
|
||||||
get => GetValue(StateProperty);
|
get => GetValue(StateProperty);
|
||||||
set => SetValue(StateProperty, value);
|
set => SetValue(StateProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<SpinnerState> StateProperty =
|
public static readonly StyledProperty<SpinnerState> StateProperty =
|
||||||
AvaloniaProperty.Register<StatusSpinner, SpinnerState>(nameof(State));
|
AvaloniaProperty.Register<StatusSpinner, SpinnerState>(nameof(State));
|
||||||
}
|
}
|
@ -4,37 +4,33 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="SPTInstaller.CustomControls.TaskDetails">
|
x:Class="SPTInstaller.CustomControls.TaskDetails">
|
||||||
<Grid RowDefinitions="10,*,AUTO,10,AUTO,*,AUTO,10" ColumnDefinitions="10,*,10">
|
<Grid RowDefinitions="10,*,AUTO,10,AUTO,*,AUTO,10" ColumnDefinitions="10,*,10">
|
||||||
|
|
||||||
<Label Grid.Column="1" Grid.Row="2"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
FontSize="15"
|
|
||||||
FontWeight="SemiBold"
|
|
||||||
Content="{Binding Message, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="1" Grid.Row="4"
|
<Label Grid.Column="1" Grid.Row="2"
|
||||||
Foreground="Gainsboro"
|
HorizontalAlignment="Center"
|
||||||
HorizontalAlignment="Center"
|
FontSize="15"
|
||||||
FontSize="12"
|
FontWeight="SemiBold"
|
||||||
Text="{Binding Details, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Content="{Binding Message, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
TextTrimming="CharacterEllipsis"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
MaxLines="3"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Grid Grid.Column="1" Grid.Row="6" ColumnDefinitions="*,AUTO">
|
<TextBlock Grid.Column="1" Grid.Row="4"
|
||||||
|
Foreground="Gainsboro"
|
||||||
<ProgressBar IsVisible="{Binding ShowProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
HorizontalAlignment="Center"
|
||||||
Value="{Binding Progress, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
FontSize="12"
|
||||||
HorizontalAlignment="Stretch"
|
Text="{Binding Details, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
IsIndeterminate="{Binding IndeterminateProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
TextTrimming="CharacterEllipsis"
|
||||||
/>
|
TextWrapping="Wrap"
|
||||||
|
MaxLines="3" />
|
||||||
|
|
||||||
<Label Grid.Column="1"
|
<Grid Grid.Column="1" Grid.Row="6" ColumnDefinitions="*,AUTO">
|
||||||
Content="{Binding Progress, RelativeSource={RelativeSource AncestorType=UserControl}, StringFormat='{}{0}%'}"
|
|
||||||
IsVisible="{Binding !IndeterminateProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
<ProgressBar IsVisible="{Binding ShowProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
/>
|
Value="{Binding Progress, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
</Grid>
|
HorizontalAlignment="Stretch"
|
||||||
</Grid>
|
IsIndeterminate="{Binding IndeterminateProgress, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
</UserControl>
|
|
||||||
|
<Label Grid.Column="1"
|
||||||
|
Content="{Binding Progress, RelativeSource={RelativeSource AncestorType=UserControl}, StringFormat='{}{0}%'}"
|
||||||
|
IsVisible="{Binding !IndeterminateProgress, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
@ -9,49 +9,49 @@ public partial class TaskDetails : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Message
|
public string Message
|
||||||
{
|
{
|
||||||
get => GetValue(MessageProperty);
|
get => GetValue(MessageProperty);
|
||||||
set => SetValue(MessageProperty, value);
|
set => SetValue(MessageProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> MessageProperty =
|
public static readonly StyledProperty<string> MessageProperty =
|
||||||
AvaloniaProperty.Register<TaskDetails, string>(nameof(Message));
|
AvaloniaProperty.Register<TaskDetails, string>(nameof(Message));
|
||||||
|
|
||||||
public string Details
|
public string Details
|
||||||
{
|
{
|
||||||
get => GetValue(DetailsProperty);
|
get => GetValue(DetailsProperty);
|
||||||
set => SetValue(DetailsProperty, value);
|
set => SetValue(DetailsProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> DetailsProperty =
|
public static readonly StyledProperty<string> DetailsProperty =
|
||||||
AvaloniaProperty.Register<TaskDetails, string>(nameof(Details));
|
AvaloniaProperty.Register<TaskDetails, string>(nameof(Details));
|
||||||
|
|
||||||
public int Progress
|
public int Progress
|
||||||
{
|
{
|
||||||
get => GetValue(ProgressProperty);
|
get => GetValue(ProgressProperty);
|
||||||
set => SetValue(ProgressProperty, value);
|
set => SetValue(ProgressProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<int> ProgressProperty =
|
public static readonly StyledProperty<int> ProgressProperty =
|
||||||
AvaloniaProperty.Register<TaskDetails, int>(nameof(Progress));
|
AvaloniaProperty.Register<TaskDetails, int>(nameof(Progress));
|
||||||
|
|
||||||
public bool ShowProgress
|
public bool ShowProgress
|
||||||
{
|
{
|
||||||
get => GetValue(ShowProgressProperty);
|
get => GetValue(ShowProgressProperty);
|
||||||
set => SetValue(ShowProgressProperty, value);
|
set => SetValue(ShowProgressProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> ShowProgressProperty =
|
public static readonly StyledProperty<bool> ShowProgressProperty =
|
||||||
AvaloniaProperty.Register<TaskDetails, bool>(nameof(ShowProgress));
|
AvaloniaProperty.Register<TaskDetails, bool>(nameof(ShowProgress));
|
||||||
|
|
||||||
public bool IndeterminateProgress
|
public bool IndeterminateProgress
|
||||||
{
|
{
|
||||||
get => GetValue(IndeterminateProgressProperty);
|
get => GetValue(IndeterminateProgressProperty);
|
||||||
set => SetValue(IndeterminateProgressProperty, value);
|
set => SetValue(IndeterminateProgressProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> IndeterminateProgressProperty =
|
public static readonly StyledProperty<bool> IndeterminateProgressProperty =
|
||||||
AvaloniaProperty.Register<TaskDetails, bool>(nameof(IndeterminateProgress));
|
AvaloniaProperty.Register<TaskDetails, bool>(nameof(IndeterminateProgress));
|
||||||
}
|
}
|
@ -5,72 +5,68 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="SPTInstaller.CustomControls.TitleBar">
|
x:Class="SPTInstaller.CustomControls.TitleBar">
|
||||||
|
|
||||||
<Grid ColumnDefinitions="AUTO,*,AUTO,AUTO">
|
<Grid ColumnDefinitions="AUTO,*,AUTO,AUTO">
|
||||||
|
|
||||||
<Rectangle Grid.ColumnSpan="6" IsHitTestVisible="False"
|
<Rectangle Grid.ColumnSpan="6" IsHitTestVisible="False"
|
||||||
Fill="{Binding Background, RelativeSource={
|
Fill="{Binding Background, RelativeSource={
|
||||||
RelativeSource AncestorType=UserControl}}"
|
RelativeSource AncestorType=UserControl}}" />
|
||||||
/>
|
|
||||||
|
|
||||||
<Label Content="{Binding Title, RelativeSource={
|
<Label Content="{Binding Title, RelativeSource={
|
||||||
RelativeSource AncestorType=UserControl}}"
|
RelativeSource AncestorType=UserControl}}"
|
||||||
IsHitTestVisible="False"
|
IsHitTestVisible="False"
|
||||||
Foreground="{Binding Foreground, RelativeSource={
|
Foreground="{Binding Foreground, RelativeSource={
|
||||||
RelativeSource AncestorType=UserControl}}"
|
RelativeSource AncestorType=UserControl}}"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
VerticalContentAlignment="Center"
|
VerticalContentAlignment="Center" />
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Minimize (-) Button -->
|
<!-- Minimize (-) Button -->
|
||||||
<Button Content="" Grid.Column="2"
|
<Button Content="" Grid.Column="2"
|
||||||
Foreground="{Binding ButtonForeground, RelativeSource={
|
Foreground="{Binding ButtonForeground, RelativeSource={
|
||||||
RelativeSource AncestorType=UserControl}}"
|
RelativeSource AncestorType=UserControl}}"
|
||||||
Command="{Binding MinButtonCommand, RelativeSource={
|
Command="{Binding MinButtonCommand, RelativeSource={
|
||||||
RelativeSource AncestorType=UserControl}}"
|
RelativeSource AncestorType=UserControl}}"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
HorizontalContentAlignment="Center"
|
HorizontalContentAlignment="Center"
|
||||||
VerticalContentAlignment="Center"
|
VerticalContentAlignment="Center"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
FontFamily="Segoe MDL2 Assets"
|
FontFamily="Segoe MDL2 Assets"
|
||||||
CornerRadius="0"
|
CornerRadius="0"
|
||||||
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 SPT_Brush_DarkGrayBlue}" />
|
||||||
<Setter Property="Background" Value="{StaticResource AKI_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 SPT_Background_Light}" />
|
||||||
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}"/>
|
</Style>
|
||||||
</Style>
|
</Button.Styles>
|
||||||
</Button.Styles>
|
</Button>
|
||||||
</Button>
|
|
||||||
|
|
||||||
<!-- Close (X) Button -->
|
<!-- Close (X) Button -->
|
||||||
<Button Content="" Grid.Column="3"
|
<Button Content="" Grid.Column="3"
|
||||||
Foreground="{Binding ButtonForeground, RelativeSource={
|
Foreground="{Binding ButtonForeground, RelativeSource={
|
||||||
RelativeSource AncestorType=UserControl}}"
|
RelativeSource AncestorType=UserControl}}"
|
||||||
Command="{Binding XButtonCommand, RelativeSource={
|
Command="{Binding XButtonCommand, RelativeSource={
|
||||||
RelativeSource AncestorType=UserControl}}"
|
RelativeSource AncestorType=UserControl}}"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
HorizontalContentAlignment="Center"
|
HorizontalContentAlignment="Center"
|
||||||
VerticalContentAlignment="Center"
|
VerticalContentAlignment="Center"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
FontFamily="Segoe MDL2 Assets"
|
FontFamily="Segoe MDL2 Assets"
|
||||||
CornerRadius="0"
|
CornerRadius="0"
|
||||||
Width="35"
|
Width="35">
|
||||||
>
|
<Button.Styles>
|
||||||
<Button.Styles>
|
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||||
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
<Setter Property="Background" Value="IndianRed" />
|
||||||
<Setter Property="Background" Value="IndianRed"/>
|
<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="Crimson" />
|
||||||
<Setter Property="Background" Value="Crimson"/>
|
</Style>
|
||||||
</Style>
|
</Button.Styles>
|
||||||
</Button.Styles>
|
</Button>
|
||||||
</Button>
|
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
@ -12,62 +12,62 @@ public partial class TitleBar : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<string> TitleProperty =
|
public static readonly StyledProperty<string> TitleProperty =
|
||||||
AvaloniaProperty.Register<TitleBar, string>(nameof(Title));
|
AvaloniaProperty.Register<TitleBar, string>(nameof(Title));
|
||||||
|
|
||||||
public string Title
|
public string Title
|
||||||
{
|
{
|
||||||
get => GetValue(TitleProperty);
|
get => GetValue(TitleProperty);
|
||||||
set => SetValue(TitleProperty, value);
|
set => SetValue(TitleProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<IBrush> ButtonForegroundProperty =
|
public static readonly StyledProperty<IBrush> ButtonForegroundProperty =
|
||||||
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(ButtonForeground));
|
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(ButtonForeground));
|
||||||
|
|
||||||
public IBrush ButtonForeground
|
public IBrush ButtonForeground
|
||||||
{
|
{
|
||||||
get => GetValue(ButtonForegroundProperty);
|
get => GetValue(ButtonForegroundProperty);
|
||||||
set => SetValue(ButtonForegroundProperty, value);
|
set => SetValue(ButtonForegroundProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static new readonly StyledProperty<IBrush> ForegroundProperty =
|
public static new readonly StyledProperty<IBrush> ForegroundProperty =
|
||||||
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Foreground));
|
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Foreground));
|
||||||
|
|
||||||
public new IBrush Foreground
|
public new IBrush Foreground
|
||||||
{
|
{
|
||||||
get => GetValue(ForegroundProperty);
|
get => GetValue(ForegroundProperty);
|
||||||
set => SetValue(ForegroundProperty, value);
|
set => SetValue(ForegroundProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static new readonly StyledProperty<IBrush> BackgroundProperty =
|
public static new readonly StyledProperty<IBrush> BackgroundProperty =
|
||||||
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Background));
|
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Background));
|
||||||
|
|
||||||
public new IBrush Background
|
public new IBrush Background
|
||||||
{
|
{
|
||||||
get => GetValue(BackgroundProperty);
|
get => GetValue(BackgroundProperty);
|
||||||
set => SetValue(BackgroundProperty, value);
|
set => SetValue(BackgroundProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Close Button Command (X Button) Property
|
//Close Button Command (X Button) Property
|
||||||
public static readonly StyledProperty<ICommand> XButtonCommandProperty =
|
public static readonly StyledProperty<ICommand> XButtonCommandProperty =
|
||||||
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(XButtonCommand));
|
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(XButtonCommand));
|
||||||
|
|
||||||
public ICommand XButtonCommand
|
public ICommand XButtonCommand
|
||||||
{
|
{
|
||||||
get => GetValue(XButtonCommandProperty);
|
get => GetValue(XButtonCommandProperty);
|
||||||
set => SetValue(XButtonCommandProperty, value);
|
set => SetValue(XButtonCommandProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Minimize Button Command (- Button) Property
|
//Minimize Button Command (- Button) Property
|
||||||
public static readonly StyledProperty<ICommand> MinButtonCommandProperty =
|
public static readonly StyledProperty<ICommand> MinButtonCommandProperty =
|
||||||
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(MinButtonCommand));
|
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(MinButtonCommand));
|
||||||
|
|
||||||
public ICommand MinButtonCommand
|
public ICommand MinButtonCommand
|
||||||
{
|
{
|
||||||
get => GetValue(MinButtonCommandProperty);
|
get => GetValue(MinButtonCommandProperty);
|
||||||
|
46
SPTInstaller/CustomControls/UpdateButton.axaml
Normal file
46
SPTInstaller/CustomControls/UpdateButton.axaml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="SPTInstaller.CustomControls.UpdateButton">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<StyleInclude Source="../Assets/Styles.axaml" />
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
|
||||||
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<StackPanel.IsVisible>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="!Updating" RelativeSource="{RelativeSource AncestorType=UserControl}" />
|
||||||
|
<Binding Path="UpdateAvailable" RelativeSource="{RelativeSource AncestorType=UserControl}" />
|
||||||
|
</MultiBinding>
|
||||||
|
</StackPanel.IsVisible>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="{Binding InfoText, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CornerRadius="20 0 0 20"
|
||||||
|
Classes="yellow"
|
||||||
|
Command="{Binding UpdateCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
|
|
||||||
|
<Button Content="Not now" CornerRadius="0 20 20 0"
|
||||||
|
Command="{Binding DismissCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Panel Margin="0 10">
|
||||||
|
<Panel.IsVisible>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.Or}">
|
||||||
|
<Binding Path="Updating" RelativeSource="{RelativeSource AncestorType=UserControl}" />
|
||||||
|
<Binding Path="CheckingForUpdate" RelativeSource="{RelativeSource AncestorType=UserControl}" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Panel.IsVisible>
|
||||||
|
<ProgressBar CornerRadius="20" VerticalAlignment="Stretch"
|
||||||
|
Value="{Binding DownloadProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
IsIndeterminate="{Binding IsIndeterminate, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||||
|
<Label Content="{Binding InfoText, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||||
|
Foreground="Black" FontWeight="SemiBold" />
|
||||||
|
</Panel>
|
||||||
|
</Panel>
|
||||||
|
</UserControl>
|
88
SPTInstaller/CustomControls/UpdateButton.axaml.cs
Normal file
88
SPTInstaller/CustomControls/UpdateButton.axaml.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace SPTInstaller.CustomControls;
|
||||||
|
|
||||||
|
public partial class UpdateButton : UserControl
|
||||||
|
{
|
||||||
|
public UpdateButton()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<string> InfoTextProperty = AvaloniaProperty.Register<UpdateButton, string>(
|
||||||
|
"InfoText");
|
||||||
|
|
||||||
|
public string InfoText
|
||||||
|
{
|
||||||
|
get => GetValue(InfoTextProperty);
|
||||||
|
set => SetValue(InfoTextProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> CheckingForUpdateProperty =
|
||||||
|
AvaloniaProperty.Register<UpdateButton, bool>(
|
||||||
|
"CheckingForUpdate");
|
||||||
|
|
||||||
|
public bool CheckingForUpdate
|
||||||
|
{
|
||||||
|
get => GetValue(CheckingForUpdateProperty);
|
||||||
|
set => SetValue(CheckingForUpdateProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<ICommand> DismissCommandProperty =
|
||||||
|
AvaloniaProperty.Register<UpdateButton, ICommand>(
|
||||||
|
"DismissCommand");
|
||||||
|
|
||||||
|
public ICommand DismissCommand
|
||||||
|
{
|
||||||
|
get => GetValue(DismissCommandProperty);
|
||||||
|
set => SetValue(DismissCommandProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<ICommand> UpdateCommandProperty =
|
||||||
|
AvaloniaProperty.Register<UpdateButton, ICommand>(
|
||||||
|
"UpdateCommand");
|
||||||
|
|
||||||
|
public ICommand UpdateCommand
|
||||||
|
{
|
||||||
|
get => GetValue(UpdateCommandProperty);
|
||||||
|
set => SetValue(UpdateCommandProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> UpdatingProperty = AvaloniaProperty.Register<UpdateButton, bool>(
|
||||||
|
"Updating");
|
||||||
|
|
||||||
|
public bool Updating
|
||||||
|
{
|
||||||
|
get => GetValue(UpdatingProperty);
|
||||||
|
set => SetValue(UpdatingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<int> DownloadProgressProperty = AvaloniaProperty.Register<UpdateButton, int>(
|
||||||
|
"DownloadProgress");
|
||||||
|
|
||||||
|
public int DownloadProgress
|
||||||
|
{
|
||||||
|
get => GetValue(DownloadProgressProperty);
|
||||||
|
set => SetValue(DownloadProgressProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> IsIndeterminateProperty = AvaloniaProperty.Register<UpdateButton, bool>(
|
||||||
|
"IsIndeterminate");
|
||||||
|
|
||||||
|
public bool IsIndeterminate
|
||||||
|
{
|
||||||
|
get => GetValue(IsIndeterminateProperty);
|
||||||
|
set => SetValue(IsIndeterminateProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> UpdateAvailableProperty = AvaloniaProperty.Register<UpdateButton, bool>(
|
||||||
|
"UpdateAvailable");
|
||||||
|
|
||||||
|
public bool UpdateAvailable
|
||||||
|
{
|
||||||
|
get => GetValue(UpdateAvailableProperty);
|
||||||
|
set => SetValue(UpdateAvailableProperty, value);
|
||||||
|
}
|
||||||
|
}
|
@ -1,65 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="SPTInstaller.CustomControls.UpdateInfoCard"
|
|
||||||
MinHeight="100" MinWidth="300">
|
|
||||||
|
|
||||||
<UserControl.Styles>
|
|
||||||
<Style Selector="Grid">
|
|
||||||
<Setter Property="Opacity" Value="0"/>
|
|
||||||
<Setter Property="Transitions">
|
|
||||||
<Setter.Value>
|
|
||||||
<Transitions>
|
|
||||||
<DoubleTransition Property="Opacity" Duration="0:0:0.2"/>
|
|
||||||
</Transitions>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Grid.show">
|
|
||||||
<Setter Property="Opacity" Value="1"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ProgressBar">
|
|
||||||
<Setter Property="IsVisible" Value="False"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ProgressBar.checking">
|
|
||||||
<Setter Property="IsIndeterminate" Value="True"/>
|
|
||||||
<Setter Property="IsVisible" Value="True"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ProgressBar.updating">
|
|
||||||
<Setter Property="IsIndeterminate" Value="false"/>
|
|
||||||
<Setter Property="IsVisible" Value="True"/>
|
|
||||||
</Style>
|
|
||||||
</UserControl.Styles>
|
|
||||||
|
|
||||||
<Grid ColumnDefinitions="10,*,AUTO,AUTO,10" RowDefinitions="10,AUTO,AUTO,10"
|
|
||||||
Classes.show="{Binding ShowUpdateCard, RelativeSource={RelativeSource AncestorType=UserControl}}">
|
|
||||||
<Border Grid.ColumnSpan="5" Grid.RowSpan="4" Background="{StaticResource AKI_Background_Light}"
|
|
||||||
BoxShadow="2 2 10 .1 black" CornerRadius="8"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" MaxWidth="400"
|
|
||||||
Text="{Binding InfoText, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
TextWrapping="Wrap" Margin="0 10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button Grid.Column="2" Grid.Row="2" Content="Not now"
|
|
||||||
Classes="outlined"
|
|
||||||
IsVisible="{Binding UpdateAvailable, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
Command="{Binding NotNowCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
/>
|
|
||||||
<Button Grid.Column="3" Grid.Row="2" Content="Update"
|
|
||||||
Classes="yellow" Margin="10 0 0 0"
|
|
||||||
IsVisible="{Binding UpdateAvailable, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
Command="{Binding UpdateInstallerCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProgressBar Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="3"
|
|
||||||
Value="{Binding DownloadProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
Classes.updating="{Binding Updating, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
Classes.checking="{Binding IndeterminateProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
@ -1,76 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using System.Windows.Input;
|
|
||||||
|
|
||||||
namespace SPTInstaller.CustomControls;
|
|
||||||
public partial class UpdateInfoCard : UserControl
|
|
||||||
{
|
|
||||||
public UpdateInfoCard()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShowUpdateCard
|
|
||||||
{
|
|
||||||
get => GetValue(ShowUpdateCardProperty);
|
|
||||||
set => SetValue(ShowUpdateCardProperty, value);
|
|
||||||
}
|
|
||||||
public static readonly StyledProperty<bool> ShowUpdateCardProperty =
|
|
||||||
AvaloniaProperty.Register<UpdateInfoCard, bool>(nameof(ShowUpdateCard));
|
|
||||||
|
|
||||||
public bool Updating
|
|
||||||
{
|
|
||||||
get => GetValue(UpdatingProperty);
|
|
||||||
set => SetValue(UpdatingProperty, value);
|
|
||||||
}
|
|
||||||
public static readonly StyledProperty<bool> UpdatingProperty =
|
|
||||||
AvaloniaProperty.Register<UpdateInfoCard, bool>(nameof(Updating));
|
|
||||||
|
|
||||||
public bool UpdateAvailable
|
|
||||||
{
|
|
||||||
get => GetValue(UpdateAvailableProperty);
|
|
||||||
set => SetValue(UpdateAvailableProperty, value);
|
|
||||||
}
|
|
||||||
public static readonly StyledProperty<bool> UpdateAvailableProperty =
|
|
||||||
AvaloniaProperty.Register<UpdateInfoCard, bool>(nameof(UpdateAvailable));
|
|
||||||
|
|
||||||
public bool IndeterminateProgress
|
|
||||||
{
|
|
||||||
get => GetValue(IndeterminateProgressProperty);
|
|
||||||
set => SetValue(IndeterminateProgressProperty, value);
|
|
||||||
}
|
|
||||||
public static readonly StyledProperty<bool> IndeterminateProgressProperty =
|
|
||||||
AvaloniaProperty.Register<UpdateInfoCard, bool>(nameof(IndeterminateProgress));
|
|
||||||
|
|
||||||
public string InfoText
|
|
||||||
{
|
|
||||||
get => GetValue(InfoTextProperty);
|
|
||||||
set => SetValue(InfoTextProperty, value);
|
|
||||||
}
|
|
||||||
public static readonly StyledProperty<string> InfoTextProperty =
|
|
||||||
AvaloniaProperty.Register<UpdateInfoCard, string>(nameof(InfoText));
|
|
||||||
|
|
||||||
public int DownloadProgress
|
|
||||||
{
|
|
||||||
get => GetValue(DownloadProgressProperty);
|
|
||||||
set => SetValue(DownloadProgressProperty, value);
|
|
||||||
}
|
|
||||||
public static readonly StyledProperty<int> DownloadProgressProperty =
|
|
||||||
AvaloniaProperty.Register<UpdateInfoCard, int>(nameof(DownloadProgress));
|
|
||||||
|
|
||||||
public ICommand NotNowCommand
|
|
||||||
{
|
|
||||||
get => GetValue(NotNowCommandProperty);
|
|
||||||
set => SetValue(NotNowCommandProperty, value);
|
|
||||||
}
|
|
||||||
public static readonly StyledProperty<ICommand> NotNowCommandProperty =
|
|
||||||
AvaloniaProperty.Register<UpdateInfoCard, ICommand>(nameof(NotNowCommand));
|
|
||||||
|
|
||||||
public ICommand UpdateInstallerCommand
|
|
||||||
{
|
|
||||||
get => GetValue(UpdateInstallerCommandProperty);
|
|
||||||
set => SetValue(UpdateInstallerCommandProperty, value);
|
|
||||||
}
|
|
||||||
public static readonly StyledProperty<ICommand> UpdateInstallerCommandProperty =
|
|
||||||
AvaloniaProperty.Register<UpdateInfoCard, ICommand>(nameof(UpdateInstallerCommand));
|
|
||||||
}
|
|
@ -1,3 +1,4 @@
|
|||||||
// Global using directives
|
// Global using directives
|
||||||
|
|
||||||
global using System;
|
global using System;
|
||||||
global using System.IO;
|
global using System.IO;
|
@ -8,32 +8,32 @@ public static class DirectorySizeHelper
|
|||||||
// SizeSuffix implementation found here:
|
// SizeSuffix implementation found here:
|
||||||
// https://stackoverflow.com/a/14488941
|
// https://stackoverflow.com/a/14488941
|
||||||
static readonly string[] SizeSuffixes =
|
static readonly string[] SizeSuffixes =
|
||||||
{ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
|
{ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
|
||||||
|
|
||||||
public static string SizeSuffix(Int64 value, int decimalPlaces = 1)
|
public static string SizeSuffix(Int64 value, int decimalPlaces = 1)
|
||||||
{
|
{
|
||||||
if (decimalPlaces < 0)
|
if (decimalPlaces < 0)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException("decimalPlaces");
|
throw new ArgumentOutOfRangeException("decimalPlaces");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value < 0)
|
if (value < 0)
|
||||||
{
|
{
|
||||||
return "-" + SizeSuffix(-value, decimalPlaces);
|
return "-" + SizeSuffix(-value, decimalPlaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value == 0)
|
if (value == 0)
|
||||||
{
|
{
|
||||||
return string.Format("{0:n" + decimalPlaces + "} bytes", 0);
|
return string.Format("{0:n" + decimalPlaces + "} bytes", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// mag is 0 for bytes, 1 for KB, 2, for MB, etc.
|
// mag is 0 for bytes, 1 for KB, 2, for MB, etc.
|
||||||
int mag = (int)Math.Log(value, 1024);
|
int mag = (int)Math.Log(value, 1024);
|
||||||
|
|
||||||
// 1L << (mag * 10) == 2 ^ (10 * mag)
|
// 1L << (mag * 10) == 2 ^ (10 * mag)
|
||||||
// [i.e. the number of bytes in the unit corresponding to mag]
|
// [i.e. the number of bytes in the unit corresponding to mag]
|
||||||
decimal adjustedSize = (decimal)value / (1L << (mag * 10));
|
decimal adjustedSize = (decimal)value / (1L << (mag * 10));
|
||||||
|
|
||||||
// make adjustment when the value is large enough that
|
// make adjustment when the value is large enough that
|
||||||
// it would round up to 1000 or more
|
// it would round up to 1000 or more
|
||||||
if (Math.Round(adjustedSize, decimalPlaces) >= 1000)
|
if (Math.Round(adjustedSize, decimalPlaces) >= 1000)
|
||||||
@ -41,12 +41,12 @@ public static class DirectorySizeHelper
|
|||||||
mag += 1;
|
mag += 1;
|
||||||
adjustedSize /= 1024;
|
adjustedSize /= 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Format("{0:n" + decimalPlaces + "} {1}",
|
return string.Format("{0:n" + decimalPlaces + "} {1}",
|
||||||
adjustedSize,
|
adjustedSize,
|
||||||
SizeSuffixes[mag]);
|
SizeSuffixes[mag]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the size of a directory in bytes
|
/// Gets the size of a directory in bytes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
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;
|
||||||
|
|
||||||
public static class DownloadCacheHelper
|
public static class DownloadCacheHelper
|
||||||
{
|
{
|
||||||
private static HttpClient _httpClient = new() { Timeout = TimeSpan.FromHours(1) };
|
private static HttpClient _httpClient = new() { Timeout = TimeSpan.FromMinutes(15) };
|
||||||
|
|
||||||
public static string CachePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "spt-installer/cache");
|
|
||||||
|
|
||||||
|
public static TimeSpan SuggestedTtl = TimeSpan.FromHours(1);
|
||||||
|
public static string CachePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"spt-installer/cache");
|
||||||
|
|
||||||
|
public static string ReleaseMirrorUrl = "https://spt-releases.modd.in/release.json";
|
||||||
|
public static string PatchMirrorUrl = "https://slugma.waffle-lord.net/mirrors.json";
|
||||||
|
public static string InstallerUrl = "https://ligma.waffle-lord.net/SPTInstaller.exe";
|
||||||
|
public static string InstallerInfoUrl = "https://ligma.waffle-lord.net/installer.json";
|
||||||
|
|
||||||
public static string GetCacheSizeText()
|
public static string GetCacheSizeText()
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(CachePath))
|
if (!Directory.Exists(CachePath))
|
||||||
@ -19,24 +25,24 @@ public static class DownloadCacheHelper
|
|||||||
Log.Information(message);
|
Log.Information(message);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cacheDir = new DirectoryInfo(CachePath);
|
var cacheDir = new DirectoryInfo(CachePath);
|
||||||
|
|
||||||
var cacheSize = DirectorySizeHelper.GetSizeOfDirectory(cacheDir);
|
var cacheSize = DirectorySizeHelper.GetSizeOfDirectory(cacheDir);
|
||||||
|
|
||||||
if (cacheSize == -1)
|
if (cacheSize == -1)
|
||||||
{
|
{
|
||||||
var message = "An error occurred while getting the cache size :(";
|
var message = "An error occurred while getting the cache size :(";
|
||||||
Log.Error(message);
|
Log.Error(message);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cacheSize == 0)
|
if (cacheSize == 0)
|
||||||
return "Empty";
|
return "Empty";
|
||||||
|
|
||||||
return DirectorySizeHelper.SizeSuffix(cacheSize);
|
return DirectorySizeHelper.SizeSuffix(cacheSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if a file in the cache already exists
|
/// Check if a file in the cache already exists
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -44,39 +50,80 @@ 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;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
cacheFile.Refresh();
|
cacheFile.Refresh();
|
||||||
Directory.CreateDirectory(CachePath);
|
Directory.CreateDirectory(CachePath);
|
||||||
|
|
||||||
if (!cacheFile.Exists || expectedHash == null)
|
if (!cacheFile.Exists || expectedHash == null)
|
||||||
{
|
{
|
||||||
Log.Information($"{cacheFile.Name} {(cacheFile.Exists ? "is in cache" : "NOT in cache")}");
|
Log.Information($"{cacheFile.Name} {(cacheFile.Exists ? "is in cache" : "NOT in cache")}");
|
||||||
Log.Information($"Expected hash: {(expectedHash == null ? "not provided" : expectedHash)}");
|
Log.Information($"Expected hash: {(expectedHash == null ? "not provided" : expectedHash)}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FileHashHelper.CheckHash(cacheFile, expectedHash))
|
if (FileHashHelper.CheckHash(cacheFile, expectedHash))
|
||||||
{
|
{
|
||||||
fileInCache = cacheFile;
|
fileInCache = cacheFile;
|
||||||
|
Log.Information("Hashes MATCH");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.Warning("Hashes DO NOT MATCH");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Log.Error(ex, "Something went wrong during hashing");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
@ -85,28 +132,42 @@ public static class DownloadCacheHelper
|
|||||||
/// <param name="progress">A provider for progress updates</param>
|
/// <param name="progress">A provider for progress updates</param>
|
||||||
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
|
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
|
||||||
/// <remarks>If the file exists, it is deleted before downloading</remarks>
|
/// <remarks>If the file exists, it is deleted before downloading</remarks>
|
||||||
public static async Task<FileInfo?> DownloadFileAsync(string outputFileName, string targetLink, IProgress<double> progress)
|
public static async Task<FileInfo?> DownloadFileAsync(string outputFileName, string targetLink,
|
||||||
|
IProgress<double> progress)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(CachePath);
|
Directory.CreateDirectory(CachePath);
|
||||||
var outputFile = new FileInfo(Path.Join(CachePath, outputFileName));
|
var outputFile = new FileInfo(Path.Join(CachePath, outputFileName));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (outputFile.Exists)
|
if (outputFile.Exists)
|
||||||
outputFile.Delete();
|
outputFile.Delete();
|
||||||
|
|
||||||
// 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();
|
||||||
|
|
||||||
if (!outputFile.Exists)
|
if (!outputFile.Exists)
|
||||||
{
|
{
|
||||||
Log.Error("Failed to download file from url: {name} :: {url}", outputFileName, targetLink);
|
Log.Error("Failed to download file from url: {name} :: {url}", outputFileName, targetLink);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return outputFile;
|
return outputFile;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -115,7 +176,7 @@ public static class DownloadCacheHelper
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Download a file to the cache folder
|
/// Download a file to the cache folder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -127,36 +188,64 @@ public static class DownloadCacheHelper
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory(CachePath);
|
Directory.CreateDirectory(CachePath);
|
||||||
var outputFile = new FileInfo(Path.Join(CachePath, outputFileName));
|
var outputFile = new FileInfo(Path.Join(CachePath, outputFileName));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (outputFile.Exists)
|
if (outputFile.Exists)
|
||||||
outputFile.Delete();
|
outputFile.Delete();
|
||||||
|
|
||||||
using var patcherFileStream = outputFile.Open(FileMode.Create);
|
using var patcherFileStream = outputFile.Open(FileMode.Create);
|
||||||
{
|
{
|
||||||
await downloadStream.CopyToAsync(patcherFileStream);
|
await downloadStream.CopyToAsync(patcherFileStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
patcherFileStream.Close();
|
patcherFileStream.Close();
|
||||||
|
|
||||||
outputFile.Refresh();
|
outputFile.Refresh();
|
||||||
|
|
||||||
if (!outputFile.Exists)
|
if (!outputFile.Exists)
|
||||||
{
|
{
|
||||||
Log.Error("Failed to download file from stream: {name}", outputFileName);
|
Log.Error("Failed to download file from stream: {name}", outputFileName);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return outputFile;
|
return outputFile;
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Failed to download file from stream: {fileName}", outputFileName);
|
Log.Error(ex, "Failed to download file from stream: {fileName}", outputFileName);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
@ -166,13 +255,15 @@ public static class DownloadCacheHelper
|
|||||||
/// <param name="expectedHash">The expected hash of the cached file</param>
|
/// <param name="expectedHash">The expected hash of the cached file</param>
|
||||||
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
|
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
|
||||||
/// <remarks>Use <see cref="DownloadFileAsync(string, string, IProgress{double})"/> if you don't have an expected cache file hash</remarks>
|
/// <remarks>Use <see cref="DownloadFileAsync(string, string, IProgress{double})"/> if you don't have an expected cache file hash</remarks>
|
||||||
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, string targetLink, IProgress<double> progress, string expectedHash)
|
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, string targetLink,
|
||||||
|
IProgress<double> progress, string expectedHash)
|
||||||
{
|
{
|
||||||
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)
|
||||||
@ -181,7 +272,7 @@ public static class DownloadCacheHelper
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the file from cache or download it
|
/// Get the file from cache or download it
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -190,13 +281,14 @@ public static class DownloadCacheHelper
|
|||||||
/// <param name="expectedHash">The expected hash of the cached file</param>
|
/// <param name="expectedHash">The expected hash of the cached file</param>
|
||||||
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
|
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
|
||||||
/// <remarks>Use <see cref="DownloadFileAsync(string, Stream)"/> if you don't have an expected cache file hash</remarks>
|
/// <remarks>Use <see cref="DownloadFileAsync(string, Stream)"/> if you don't have an expected cache file hash</remarks>
|
||||||
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream, string expectedHash)
|
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream,
|
||||||
|
string expectedHash)
|
||||||
{
|
{
|
||||||
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);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -1,25 +1,23 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Gitea.Model;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace SPTInstaller.Helpers;
|
namespace SPTInstaller.Helpers;
|
||||||
|
|
||||||
public static class FileHashHelper
|
public static class FileHashHelper
|
||||||
{
|
{
|
||||||
public static string? GetGiteaReleaseHash(Release release)
|
// public static string? GetGiteaReleaseHash(Release release)
|
||||||
{
|
// {
|
||||||
var regex = Regex.Match(release.Body, @"Release Hash: (?<hash>\S+)");
|
// var regex = Regex.Match(release.Body, @"Release Hash: (?<hash>\S+)");
|
||||||
|
//
|
||||||
if (regex.Success)
|
// if (regex.Success)
|
||||||
{
|
// {
|
||||||
return regex.Groups["hash"].Value;
|
// return regex.Groups["hash"].Value;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
public static bool CheckHash(FileInfo file, string expectedHash)
|
public static bool CheckHash(FileInfo file, string expectedHash)
|
||||||
{
|
{
|
||||||
using var md5Service = MD5.Create();
|
using var md5Service = MD5.Create();
|
||||||
@ -29,9 +27,9 @@ public static class FileHashHelper
|
|||||||
var expectedHashBytes = Convert.FromBase64String(expectedHash);
|
var expectedHashBytes = Convert.FromBase64String(expectedHash);
|
||||||
|
|
||||||
Log.Information($"Comparing Hashes :: S: {Convert.ToBase64String(sourceHash)} - E: {expectedHash}");
|
Log.Information($"Comparing Hashes :: S: {Convert.ToBase64String(sourceHash)} - E: {expectedHash}");
|
||||||
|
|
||||||
var matched = Enumerable.SequenceEqual(sourceHash, expectedHashBytes);
|
var matched = Enumerable.SequenceEqual(sourceHash, expectedHashBytes);
|
||||||
|
|
||||||
return matched;
|
return matched;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,114 +9,80 @@ 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 (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>[^\\]+)");
|
||||||
|
|
||||||
if (nameMatched.Success)
|
if (nameMatched.Success)
|
||||||
{
|
{
|
||||||
var name = nameMatched.Groups["NAME"].Value;
|
var name = nameMatched.Groups["NAME"].Value;
|
||||||
return path.Replace(name, "-REDACTED-");
|
return path.Replace(name, "-REDACTED-");
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir, IProgress<double> progress = null, string[] exclusions = null) =>
|
public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir,
|
||||||
|
IProgress<double> progress = null, string[] exclusions = null) =>
|
||||||
CopyDirectoryWithProgress(sourceDir, targetDir, (msg, prog) => progress?.Report(prog), exclusions);
|
CopyDirectoryWithProgress(sourceDir, targetDir, (msg, prog) => progress?.Report(prog), exclusions);
|
||||||
|
|
||||||
public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir, Action<string, int> updateCallback = null, string[] exclusions = null)
|
public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir,
|
||||||
|
Action<string, int> updateCallback = null, string[] exclusions = null)
|
||||||
{
|
{
|
||||||
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;
|
||||||
|
|
||||||
|
// 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 currentFileRelativePath = file.FullName.Replace(sourceDir.FullName, "");
|
||||||
|
|
||||||
if(!iterateDirectoriesResult.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var iterateFilesResult = IterateFiles(sourceDir, targetDir, exclusions ??= new string[0], updateCallback);
|
// don't copy .bak files
|
||||||
|
if (currentFileRelativePath.EndsWith(".bak"))
|
||||||
if (!iterateFilesResult.Succeeded) return iterateDirectoriesResult;
|
{
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -125,34 +91,36 @@ public static class FileHelper
|
|||||||
return Result.FromError(ex.Message);
|
return Result.FromError(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool StreamAssemblyResourceOut(string resourceName, string outputFilePath)
|
public static bool StreamAssemblyResourceOut(string resourceName, string outputFilePath)
|
||||||
{
|
{
|
||||||
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);
|
||||||
|
|
||||||
if (outputFile.Exists)
|
if (outputFile.Exists)
|
||||||
{
|
{
|
||||||
outputFile.Delete();
|
outputFile.Delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!outputFile.Directory.Exists)
|
if (!outputFile.Directory.Exists)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(outputFile.Directory.FullName);
|
Directory.CreateDirectory(outputFile.Directory.FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resName = assembly.GetManifestResourceNames().First(x => x.EndsWith(resourceName));
|
var resName = assembly.GetManifestResourceNames().First(x => x.EndsWith(resourceName));
|
||||||
|
|
||||||
using (FileStream fs = File.Create(outputFilePath))
|
using (FileStream fs = File.Create(outputFilePath))
|
||||||
using (Stream s = assembly.GetManifestResourceStream(resName))
|
using (Stream s = assembly.GetManifestResourceStream(resName))
|
||||||
{
|
{
|
||||||
s.CopyTo(fs);
|
s.CopyTo(fs);
|
||||||
}
|
}
|
||||||
|
|
||||||
outputFile.Refresh();
|
outputFile.Refresh();
|
||||||
|
|
||||||
return outputFile.Exists;
|
return outputFile.Exists;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -161,61 +129,69 @@ public static class FileHelper
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum PathCheckType
|
/// <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)
|
||||||
{
|
{
|
||||||
EndsWith = 0,
|
path = Path.TrimEndingDirectorySeparator(path);
|
||||||
Contains = 1,
|
|
||||||
DriveRoot = 2
|
failedCheck = new();
|
||||||
}
|
|
||||||
|
var problemPaths = new List<PathCheck>()
|
||||||
public static bool CheckPathForProblemLocations(string path, out string detectedName)
|
|
||||||
{
|
|
||||||
detectedName = "";
|
|
||||||
|
|
||||||
var problemNames = new Dictionary<string, PathCheckType>()
|
|
||||||
{
|
{
|
||||||
{ "Desktop", PathCheckType.EndsWith },
|
new("SteamApps", PathCheckType.EndsWith, PathCheckAction.Warn),
|
||||||
{ "Downloads", PathCheckType.EndsWith },
|
new("Documents", PathCheckType.EndsWith, PathCheckAction.Warn),
|
||||||
{ "Documents", PathCheckType.EndsWith},
|
new("Desktop", PathCheckType.EndsWith, PathCheckAction.Deny),
|
||||||
{ "OneDrive", PathCheckType.Contains },
|
new("Battlestate Games", PathCheckType.Contains, PathCheckAction.Deny),
|
||||||
{ "NextCloud", PathCheckType.Contains },
|
new("Desktop", PathCheckType.Contains, PathCheckAction.Warn),
|
||||||
{ "DropBox", PathCheckType.Contains },
|
new("scoped_dir", PathCheckType.Contains, PathCheckAction.Deny),
|
||||||
{ "Google", PathCheckType.Contains },
|
new("Downloads", PathCheckType.Contains, PathCheckAction.Deny),
|
||||||
{ "Program Files", PathCheckType.Contains},
|
new("OneDrive", PathCheckType.Contains, PathCheckAction.Deny),
|
||||||
{ "Program Files (x86)", PathCheckType.Contains},
|
new("NextCloud", PathCheckType.Contains, PathCheckAction.Deny),
|
||||||
{ "Drive Root", PathCheckType.DriveRoot}
|
new("DropBox", PathCheckType.Contains, PathCheckAction.Deny),
|
||||||
|
new("Google", PathCheckType.Contains, PathCheckAction.Deny),
|
||||||
|
new("Program Files", PathCheckType.Contains, PathCheckAction.Deny),
|
||||||
|
new("Program Files (x86", PathCheckType.Contains, PathCheckAction.Deny),
|
||||||
|
new(Path.Join("spt-installer", "cache"), PathCheckType.Contains, PathCheckAction.Deny),
|
||||||
|
new("Drive Root", PathCheckType.DriveRoot, PathCheckAction.Deny)
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var name in problemNames)
|
foreach (var check in problemPaths)
|
||||||
{
|
{
|
||||||
switch (name.Value)
|
switch (check.CheckType)
|
||||||
{
|
{
|
||||||
case PathCheckType.EndsWith:
|
case PathCheckType.EndsWith:
|
||||||
if (path.ToLower().EndsWith(name.Key.ToLower()))
|
if (path.ToLower().EndsWith(check.Target.ToLower()))
|
||||||
{
|
{
|
||||||
detectedName = name.Key;
|
failedCheck = check;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case PathCheckType.Contains:
|
case PathCheckType.Contains:
|
||||||
if (path.ToLower().Contains(name.Key.ToLower()))
|
if (path.ToLower().Contains(check.Target.ToLower()))
|
||||||
{
|
{
|
||||||
detectedName = name.Key;
|
failedCheck = check;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case PathCheckType.DriveRoot:
|
case PathCheckType.DriveRoot:
|
||||||
if (Regex.Match(path.ToLower(), @"^\w:(\\|\/)$").Success)
|
if (Regex.Match(path.ToLower(), @"^\w:(\\|\/)$").Success)
|
||||||
{
|
{
|
||||||
detectedName = name.Key;
|
failedCheck = check;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -6,7 +6,8 @@ namespace SPTInstaller.Helpers;
|
|||||||
|
|
||||||
public static class HttpClientProgressExtensions
|
public static class HttpClientProgressExtensions
|
||||||
{
|
{
|
||||||
public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress<double> progress = null, CancellationToken cancellationToken = default(CancellationToken))
|
public static async Task<bool> DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination,
|
||||||
|
IProgress<double> progress = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
using (var response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead))
|
using (var response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead))
|
||||||
{
|
{
|
||||||
@ -17,18 +18,22 @@ 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 => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
|
var progressWrapper = new Progress<long>(totalBytes =>
|
||||||
await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
|
progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
|
||||||
|
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, IProgress<long> progress = null, CancellationToken cancellationToken = default(CancellationToken))
|
static async Task<long> CopyToAsync(this Stream source, Stream destination, int bufferSize,
|
||||||
|
IProgress<long> progress = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
if (bufferSize < 0)
|
if (bufferSize < 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(bufferSize));
|
throw new ArgumentOutOfRangeException(nameof(bufferSize));
|
||||||
@ -40,15 +45,18 @@ public static class HttpClientProgressExtensions
|
|||||||
throw new ArgumentNullException(nameof(destination));
|
throw new ArgumentNullException(nameof(destination));
|
||||||
if (!destination.CanWrite)
|
if (!destination.CanWrite)
|
||||||
throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
|
throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
|
||||||
|
|
||||||
var buffer = new byte[bufferSize];
|
var buffer = new byte[bufferSize];
|
||||||
long totalBytesRead = 0;
|
long totalBytesRead = 0;
|
||||||
int bytesRead;
|
int bytesRead;
|
||||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
while ((bytesRead =
|
||||||
|
await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||||
{
|
{
|
||||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||||
totalBytesRead += bytesRead;
|
totalBytesRead += bytesRead;
|
||||||
progress?.Report(totalBytesRead);
|
progress?.Report(totalBytesRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return totalBytesRead;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,26 +7,31 @@ namespace SPTInstaller.Helpers;
|
|||||||
|
|
||||||
public static class PreCheckHelper
|
public static class PreCheckHelper
|
||||||
{
|
{
|
||||||
private const string registryInstall = @"Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\EscapeFromTarkov";
|
private const string registryInstall =
|
||||||
|
@"Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\EscapeFromTarkov";
|
||||||
|
|
||||||
public static string DetectOriginalGamePath()
|
public static string DetectOriginalGamePath()
|
||||||
{
|
{
|
||||||
// We can't detect the installed path on non-Windows
|
// We can't detect the installed path on non-Windows
|
||||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var uninstallStringValue = Registry.LocalMachine.OpenSubKey(registryInstall, false)
|
var uninstallStringValue = Registry.LocalMachine.OpenSubKey(registryInstall, false)
|
||||||
?.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)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string version = FileVersionInfo.GetVersionInfo(Path.Join(gamePath, "/EscapeFromTarkov.exe")).ProductVersion.Replace('-', '.').Split('.')[^2];
|
string version = FileVersionInfo.GetVersionInfo(Path.Join(gamePath, "/EscapeFromTarkov.exe")).ProductVersion
|
||||||
|
.Replace('-', '.').Split('.')[^2];
|
||||||
return Result.FromSuccess(version);
|
return Result.FromSuccess(version);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -25,45 +25,45 @@ public static class ProcessHelper
|
|||||||
return Result.FromError(
|
return Result.FromError(
|
||||||
$"Could not find executable ({executable.Name}) or working directory ({workingDir.Name})");
|
$"Could not find executable ({executable.Name}) or working directory ({workingDir.Name})");
|
||||||
}
|
}
|
||||||
|
|
||||||
var process = new Process();
|
var process = new Process();
|
||||||
process.StartInfo.FileName = executable.FullName;
|
process.StartInfo.FileName = executable.FullName;
|
||||||
process.StartInfo.WorkingDirectory = workingDir.FullName;
|
process.StartInfo.WorkingDirectory = workingDir.FullName;
|
||||||
process.EnableRaisingEvents = true;
|
process.EnableRaisingEvents = true;
|
||||||
process.StartInfo.Arguments = "autoclose";
|
process.StartInfo.Arguments = "autoclose";
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|
||||||
process.WaitForExit();
|
process.WaitForExit();
|
||||||
|
|
||||||
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!");
|
||||||
|
|
||||||
case PatcherExitCode.EftExeNotFound:
|
case PatcherExitCode.EftExeNotFound:
|
||||||
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ReadProcessResult RunAndReadProcessOutputs(string fileName, string args, int timeout = 5000)
|
public static ReadProcessResult RunAndReadProcessOutputs(string fileName, string args, int timeout = 5000)
|
||||||
{
|
{
|
||||||
using var proc = new Process();
|
using var proc = new Process();
|
||||||
|
|
||||||
proc.StartInfo = new ProcessStartInfo
|
proc.StartInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = fileName,
|
FileName = fileName,
|
||||||
@ -72,13 +72,13 @@ public static class ProcessHelper
|
|||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
CreateNoWindow = true
|
CreateNoWindow = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var outputBuilder = new StringBuilder();
|
var outputBuilder = new StringBuilder();
|
||||||
var errorBuilder = new StringBuilder();
|
var errorBuilder = new StringBuilder();
|
||||||
|
|
||||||
using AutoResetEvent outputWaitHandle = new AutoResetEvent(false);
|
using AutoResetEvent outputWaitHandle = new AutoResetEvent(false);
|
||||||
using AutoResetEvent errorWaitHandle = new AutoResetEvent(false);
|
using AutoResetEvent errorWaitHandle = new AutoResetEvent(false);
|
||||||
|
|
||||||
proc.OutputDataReceived += (s, e) =>
|
proc.OutputDataReceived += (s, e) =>
|
||||||
{
|
{
|
||||||
if (e.Data == null)
|
if (e.Data == null)
|
||||||
@ -90,7 +90,7 @@ public static class ProcessHelper
|
|||||||
outputBuilder.AppendLine(e.Data);
|
outputBuilder.AppendLine(e.Data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
proc.ErrorDataReceived += (s, e) =>
|
proc.ErrorDataReceived += (s, e) =>
|
||||||
{
|
{
|
||||||
if (e.Data == null)
|
if (e.Data == null)
|
||||||
@ -102,7 +102,7 @@ public static class ProcessHelper
|
|||||||
errorBuilder.AppendLine(e.Data);
|
errorBuilder.AppendLine(e.Data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
proc.Start();
|
proc.Start();
|
||||||
@ -111,10 +111,10 @@ public static class ProcessHelper
|
|||||||
{
|
{
|
||||||
return ReadProcessResult.FromError(ex.Message);
|
return ReadProcessResult.FromError(ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
proc.BeginOutputReadLine();
|
proc.BeginOutputReadLine();
|
||||||
proc.BeginErrorReadLine();
|
proc.BeginErrorReadLine();
|
||||||
|
|
||||||
if (!proc.WaitForExit(timeout) || !outputWaitHandle.WaitOne(timeout) || !errorWaitHandle.WaitOne(timeout))
|
if (!proc.WaitForExit(timeout) || !outputWaitHandle.WaitOne(timeout) || !errorWaitHandle.WaitOne(timeout))
|
||||||
{
|
{
|
||||||
return ReadProcessResult.FromError("Process timed out");
|
return ReadProcessResult.FromError("Process timed out");
|
||||||
|
@ -14,22 +14,22 @@ internal static class ServiceHelper
|
|||||||
private static bool TryRegisterInstance<T, T2>(object[] parameters = null)
|
private static bool TryRegisterInstance<T, T2>(object[] parameters = null)
|
||||||
{
|
{
|
||||||
var instance = Activator.CreateInstance(typeof(T2), parameters);
|
var instance = Activator.CreateInstance(typeof(T2), parameters);
|
||||||
|
|
||||||
if (instance != null)
|
if (instance != null)
|
||||||
{
|
{
|
||||||
Locator.CurrentMutable.RegisterConstant<T>((T)instance);
|
Locator.CurrentMutable.RegisterConstant<T>((T)instance);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a class as a service
|
/// Register a class as a service
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">class to register</typeparam>
|
/// <typeparam name="T">class to register</typeparam>
|
||||||
public static void Register<T>() where T : class => Register<T, T>();
|
public static void Register<T>() where T : class => Register<T, T>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a class as a service by another type
|
/// Register a class as a service by another type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -38,33 +38,33 @@ internal static class ServiceHelper
|
|||||||
public static void Register<T, T2>() where T : class
|
public static void Register<T, T2>() where T : class
|
||||||
{
|
{
|
||||||
var constructors = typeof(T2).GetConstructors();
|
var constructors = typeof(T2).GetConstructors();
|
||||||
|
|
||||||
foreach(var constructor in constructors)
|
foreach (var constructor in constructors)
|
||||||
{
|
{
|
||||||
var parmesan = constructor.GetParameters();
|
var parmesan = constructor.GetParameters();
|
||||||
|
|
||||||
if(parmesan.Length == 0)
|
if (parmesan.Length == 0)
|
||||||
{
|
{
|
||||||
if (TryRegisterInstance<T, T2>()) return;
|
if (TryRegisterInstance<T, T2>()) return;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<object> parameters = new List<object>();
|
List<object> parameters = new List<object>();
|
||||||
|
|
||||||
for(int i = 0; i < parmesan.Length; i++)
|
for (int i = 0; i < parmesan.Length; i++)
|
||||||
{
|
{
|
||||||
var parm = parmesan[i];
|
var parm = parmesan[i];
|
||||||
|
|
||||||
var parmValue = Get(parm.ParameterType);
|
var parmValue = Get(parm.ParameterType);
|
||||||
|
|
||||||
if (parmValue != null) parameters.Add(parmValue);
|
if (parmValue != null) parameters.Add(parmValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryRegisterInstance<T, T2>(parameters.ToArray())) return;
|
if (TryRegisterInstance<T, T2>(parameters.ToArray())) return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a service from splat
|
/// Get a service from splat
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -74,17 +74,17 @@ internal static class ServiceHelper
|
|||||||
public static object Get(Type type)
|
public static object Get(Type type)
|
||||||
{
|
{
|
||||||
var service = Locator.Current.GetService(type);
|
var service = Locator.Current.GetService(type);
|
||||||
|
|
||||||
if (service == null)
|
if (service == null)
|
||||||
{
|
{
|
||||||
var message = $"Could not locate service of type '{type.Name}'";
|
var message = $"Could not locate service of type '{type.Name}'";
|
||||||
Log.Error(message);
|
Log.Error(message);
|
||||||
throw new InvalidOperationException(message);
|
throw new InvalidOperationException(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a service from splat
|
/// Get a service from splat
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -94,17 +94,17 @@ internal static class ServiceHelper
|
|||||||
public static T Get<T>()
|
public static T Get<T>()
|
||||||
{
|
{
|
||||||
var service = Locator.Current.GetService<T>();
|
var service = Locator.Current.GetService<T>();
|
||||||
|
|
||||||
if (service == null)
|
if (service == null)
|
||||||
{
|
{
|
||||||
var message = $"Could not locate service of type '{nameof(T)}'";
|
var message = $"Could not locate service of type '{nameof(T)}'";
|
||||||
Log.Error(message);
|
Log.Error(message);
|
||||||
throw new InvalidOperationException(message);
|
throw new InvalidOperationException(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all services of a type
|
/// Get all services of a type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -114,14 +114,14 @@ internal static class ServiceHelper
|
|||||||
public static T[] GetAll<T>()
|
public static T[] GetAll<T>()
|
||||||
{
|
{
|
||||||
var services = Locator.Current.GetServices<T>().ToArray();
|
var services = Locator.Current.GetServices<T>().ToArray();
|
||||||
|
|
||||||
if (services == null || services.Count() == 0)
|
if (services == null || services.Count() == 0)
|
||||||
{
|
{
|
||||||
var message = $"Could not locate service of type '{nameof(T)}'";
|
var message = $"Could not locate service of type '{nameof(T)}'";
|
||||||
Log.Error(message);
|
Log.Error(message);
|
||||||
throw new InvalidOperationException(message);
|
throw new InvalidOperationException(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,32 +5,30 @@ namespace SPTInstaller.Helpers;
|
|||||||
|
|
||||||
public static class ZipHelper
|
public static class ZipHelper
|
||||||
{
|
{
|
||||||
public static Result Decompress(FileInfo archiveFile, DirectoryInfo outputDirectory, IProgress<double> progress = null)
|
public static Result Decompress(FileInfo archiveFile, DirectoryInfo outputDirectory,
|
||||||
|
IProgress<double> progress = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var archiveStream = archiveFile.OpenRead();
|
using var archiveStream = archiveFile.OpenRead();
|
||||||
|
|
||||||
var dllPath = Path.Join(DownloadCacheHelper.CachePath, "7z.dll");
|
var dllPath = Path.Join(DownloadCacheHelper.CachePath, "7z.dll");
|
||||||
|
|
||||||
SevenZipBase.SetLibraryPath(dllPath);
|
SevenZipBase.SetLibraryPath(dllPath);
|
||||||
|
|
||||||
var extractor = new SevenZipExtractor(archiveStream);
|
var extractor = new SevenZipExtractor(archiveStream);
|
||||||
|
|
||||||
extractor.Extracting += (_, args) =>
|
extractor.Extracting += (_, args) => { progress.Report(args.PercentDone); };
|
||||||
{
|
|
||||||
progress.Report(args.PercentDone);
|
|
||||||
};
|
|
||||||
|
|
||||||
extractor.ExtractArchive(outputDirectory.FullName);
|
extractor.ExtractArchive(outputDirectory.FullName);
|
||||||
|
|
||||||
outputDirectory.Refresh();
|
outputDirectory.Refresh();
|
||||||
|
|
||||||
if (!outputDirectory.Exists)
|
if (!outputDirectory.Exists)
|
||||||
{
|
{
|
||||||
return Result.FromError($"Failed to extract files: {archiveFile.Name}");
|
return Result.FromError($"Failed to extract files: {archiveFile.Name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.FromSuccess();
|
return Result.FromSuccess();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -8,22 +8,23 @@ namespace SPTInstaller.Installer_Tasks;
|
|||||||
public class CopyClientTask : InstallerTaskBase
|
public class CopyClientTask : InstallerTaskBase
|
||||||
{
|
{
|
||||||
private InternalData _data;
|
private InternalData _data;
|
||||||
|
|
||||||
public CopyClientTask(InternalData data) : base("Copy Client Files")
|
public CopyClientTask(InternalData data) : base("Copy Client Files")
|
||||||
{
|
{
|
||||||
_data = data;
|
_data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<IResult> TaskOperation()
|
public override async Task<IResult> TaskOperation()
|
||||||
{
|
{
|
||||||
SetStatus("Copying Client Files", "", 0);
|
SetStatus("Copying Client Files", "", 0);
|
||||||
|
|
||||||
var originalGameDirInfo = new DirectoryInfo(_data.OriginalGamePath);
|
var originalGameDirInfo = new DirectoryInfo(_data.OriginalGamePath);
|
||||||
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
|
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
|
||||||
|
|
||||||
// relative path for exclusions
|
// relative path for exclusions
|
||||||
var exclusions = new[] { "\\Logs" };
|
var exclusions = new[] { "\\Logs" };
|
||||||
|
|
||||||
return FileHelper.CopyDirectoryWithProgress(originalGameDirInfo, targetInstallDirInfo, (message, progress) => { SetStatus(null, message, progress, null, true); }, exclusions);
|
return FileHelper.CopyDirectoryWithProgress(originalGameDirInfo, targetInstallDirInfo,
|
||||||
|
(message, progress) => { SetStatus(null, message, progress, null, true); }, exclusions);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,37 +15,21 @@ public class DownloadTask : InstallerTaskBase
|
|||||||
private InternalData _data;
|
private InternalData _data;
|
||||||
private List<IMirrorDownloader> _mirrors = new List<IMirrorDownloader>();
|
private List<IMirrorDownloader> _mirrors = new List<IMirrorDownloader>();
|
||||||
private string _expectedPatcherHash = "";
|
private string _expectedPatcherHash = "";
|
||||||
|
|
||||||
public DownloadTask(InternalData data) : base("Download Files")
|
public DownloadTask(InternalData data) : base("Download Files")
|
||||||
{
|
{
|
||||||
_data = data;
|
_data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IResult> BuildMirrorList()
|
private async Task<IResult> BuildMirrorList()
|
||||||
{
|
{
|
||||||
var progress = new Progress<double>((d) => { SetStatus("Downloading Mirror List", "", (int)Math.Floor(d), ProgressStyle.Shown);});
|
foreach (var mirror in _data.PatchInfo.Mirrors)
|
||||||
|
|
||||||
var file = await DownloadCacheHelper.DownloadFileAsync("mirrors.json", _data.PatcherMirrorsLink, progress);
|
|
||||||
|
|
||||||
if (file == null)
|
|
||||||
{
|
|
||||||
return Result.FromError("Failed to download mirror list");
|
|
||||||
}
|
|
||||||
|
|
||||||
var mirrorsList = JsonConvert.DeserializeObject<List<DownloadMirror>>(File.ReadAllText(file.FullName));
|
|
||||||
|
|
||||||
if (mirrorsList == null)
|
|
||||||
{
|
|
||||||
return Result.FromError("Failed to deserialize mirrors list");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var mirror in mirrorsList)
|
|
||||||
{
|
{
|
||||||
_expectedPatcherHash = mirror.Hash;
|
_expectedPatcherHash = mirror.Hash;
|
||||||
|
|
||||||
switch (mirror.Link)
|
switch (mirror.Link)
|
||||||
{
|
{
|
||||||
case string l when l.StartsWith("https://mega"):
|
case { } l when l.StartsWith("https://mega"):
|
||||||
_mirrors.Add(new MegaMirrorDownloader(mirror));
|
_mirrors.Add(new MegaMirrorDownloader(mirror));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -53,68 +37,79 @@ public class DownloadTask : InstallerTaskBase
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.FromSuccess("Mirrors list ready");
|
return Result.FromSuccess("Mirrors list ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IResult> DownloadPatcherFromMirrors(IProgress<double> progress)
|
private async Task<IResult> DownloadPatcherFromMirrors(IProgress<double> progress)
|
||||||
{
|
{
|
||||||
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, _expectedPatcherHash);
|
Log.Information("Using cached file {fileName} - Hash: {hash}", _data.PatcherZipInfo.Name,
|
||||||
|
_expectedPatcherHash);
|
||||||
return Result.FromSuccess();
|
return Result.FromSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var mirror in _mirrors)
|
foreach (var mirror in _mirrors)
|
||||||
{
|
{
|
||||||
SetStatus("Downloading Patcher", mirror.MirrorInfo.Link, progressStyle: ProgressStyle.Indeterminate);
|
SetStatus("Downloading Patcher", mirror.MirrorInfo.Link, progressStyle: ProgressStyle.Indeterminate);
|
||||||
|
|
||||||
_data.PatcherZipInfo = await mirror.Download(progress);
|
_data.PatcherZipInfo = await mirror.Download(progress);
|
||||||
|
|
||||||
if (_data.PatcherZipInfo != null)
|
if (_data.PatcherZipInfo != null)
|
||||||
{
|
{
|
||||||
return Result.FromSuccess();
|
return Result.FromSuccess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.FromError("Failed to download Patcher");
|
return Result.FromError("Failed to download Patcher");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IResult> DownloadSPTFromMirrors(IProgress<double> progress)
|
||||||
|
{
|
||||||
|
// Note that GetOrDownloadFileAsync handles the cached file hash check, so we don't need to check it first
|
||||||
|
foreach (var mirror in _data.ReleaseInfo.Mirrors)
|
||||||
|
{
|
||||||
|
SetStatus("Downloading SPT", mirror.DownloadUrl, progressStyle: ProgressStyle.Indeterminate);
|
||||||
|
|
||||||
|
_data.SPTZipInfo =
|
||||||
|
await DownloadCacheHelper.GetOrDownloadFileAsync("SPT", mirror.DownloadUrl, progress, mirror.Hash);
|
||||||
|
|
||||||
|
if (_data.SPTZipInfo != null)
|
||||||
|
{
|
||||||
|
return Result.FromSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.FromError("Failed to download SPT");
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<IResult> TaskOperation()
|
public override async Task<IResult> TaskOperation()
|
||||||
{
|
{
|
||||||
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)); });
|
||||||
|
|
||||||
if (_data.PatchNeeded)
|
if (_data.PatchNeeded)
|
||||||
{
|
{
|
||||||
var buildResult = await BuildMirrorList();
|
var buildResult = await BuildMirrorList();
|
||||||
|
|
||||||
if (!buildResult.Succeeded)
|
if (!buildResult.Succeeded)
|
||||||
{
|
{
|
||||||
return buildResult;
|
return buildResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
SetStatus(null, null, 0);
|
SetStatus(null, null, 0);
|
||||||
|
|
||||||
var patcherDownloadRresult = await DownloadPatcherFromMirrors(progress);
|
var patcherDownloadRresult = await DownloadPatcherFromMirrors(progress);
|
||||||
|
|
||||||
if (!patcherDownloadRresult.Succeeded)
|
if (!patcherDownloadRresult.Succeeded)
|
||||||
{
|
{
|
||||||
return patcherDownloadRresult;
|
return patcherDownloadRresult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetStatus("Downloading SPT-AKI", _data.AkiReleaseDownloadLink, 0);
|
return await DownloadSPTFromMirrors(progress);
|
||||||
|
|
||||||
_data.AkiZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki", _data.AkiReleaseDownloadLink, progress, _data.AkiReleaseHash);
|
|
||||||
|
|
||||||
if (_data.AkiZipInfo == null)
|
|
||||||
{
|
|
||||||
return Result.FromError("Failed to download spt-aki");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.FromSuccess();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,37 +8,39 @@ namespace SPTInstaller.Installer_Tasks;
|
|||||||
public class InitializationTask : InstallerTaskBase
|
public class InitializationTask : InstallerTaskBase
|
||||||
{
|
{
|
||||||
private InternalData _data;
|
private InternalData _data;
|
||||||
|
|
||||||
public InitializationTask(InternalData data) : base("Startup")
|
public InitializationTask(InternalData data) : base("Startup")
|
||||||
{
|
{
|
||||||
_data = data;
|
_data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<IResult> TaskOperation()
|
public override async Task<IResult> TaskOperation()
|
||||||
{
|
{
|
||||||
SetStatus("Initializing", $"Installed EFT Game Path: {FileHelper.GetRedactedPath(_data.OriginalGamePath)}");
|
SetStatus("Initializing", $"Installed EFT Game Path: {FileHelper.GetRedactedPath(_data.OriginalGamePath)}");
|
||||||
|
|
||||||
var result = PreCheckHelper.DetectOriginalGameVersion(_data.OriginalGamePath);
|
var result = PreCheckHelper.DetectOriginalGameVersion(_data.OriginalGamePath);
|
||||||
|
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
_data.OriginalGameVersion = result.Message;
|
_data.OriginalGameVersion = result.Message;
|
||||||
|
|
||||||
SetStatus(null, $"Installed EFT Game Version: {_data.OriginalGameVersion}");
|
SetStatus(null, $"Installed EFT Game Version: {_data.OriginalGameVersion}");
|
||||||
|
|
||||||
if (_data.OriginalGamePath == null)
|
if (_data.OriginalGamePath == null)
|
||||||
{
|
{
|
||||||
return Result.FromError("Unable to find original EFT directory, please make sure EFT is installed. Please also run EFT once");
|
return Result.FromError(
|
||||||
|
"Unable to find original EFT directory, please make sure EFT is installed. Please also run EFT once");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File.Exists(Path.Join(_data.TargetInstallPath, "EscapeFromTarkov.exe")))
|
if (File.Exists(Path.Join(_data.TargetInstallPath, "EscapeFromTarkov.exe")))
|
||||||
{
|
{
|
||||||
return Result.FromError("Installer is located in a folder that has existing game files. Please make sure the installer is in a fresh folder as per the guide");
|
return Result.FromError(
|
||||||
|
"Install location is a folder that has existing game files. Please make sure the folder doesn't contain an existing SPT install");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.FromSuccess($"Current Game Version: {_data.OriginalGameVersion}");
|
return Result.FromSuccess($"Current Game Version: {_data.OriginalGameVersion}");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -10,34 +10,34 @@ public class EftLauncherPreCheck : PreCheckBase
|
|||||||
public EftLauncherPreCheck() : base("EFT Launcher Closed", true)
|
public EftLauncherPreCheck() : base("EFT Launcher Closed", true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async override Task<PreCheckResult> CheckOperation()
|
public async override Task<PreCheckResult> CheckOperation()
|
||||||
{
|
{
|
||||||
var eftLauncherProcs = Process.GetProcessesByName("BsgLauncher");
|
var eftLauncherProcs = Process.GetProcessesByName("BsgLauncher");
|
||||||
|
|
||||||
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",
|
||||||
() =>
|
() =>
|
||||||
{
|
|
||||||
var bsgLauncherProcs = Process.GetProcessesByName("BsgLauncher");
|
|
||||||
|
|
||||||
foreach (var proc in bsgLauncherProcs)
|
|
||||||
{
|
{
|
||||||
try
|
var bsgLauncherProcs = Process.GetProcessesByName("BsgLauncher");
|
||||||
|
|
||||||
|
foreach (var proc in bsgLauncherProcs)
|
||||||
{
|
{
|
||||||
proc.Kill();
|
try
|
||||||
proc.WaitForExit();
|
{
|
||||||
Log.Information($"Killed Proc: {proc.ProcessName}#{proc.Id}");
|
proc.Kill();
|
||||||
|
proc.WaitForExit();
|
||||||
|
Log.Information($"Killed Proc: {proc.ProcessName}#{proc.Id}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, $"Failed to kill proc: {proc.ProcessName}#{proc.Id}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
RequestReevaluation();
|
||||||
Log.Error(ex, $"Failed to kill proc: {proc.ProcessName}#{proc.Id}");
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RequestReevaluation();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,46 +8,85 @@ namespace SPTInstaller.Installer_Tasks.PreChecks;
|
|||||||
public class FreeSpacePreCheck : PreCheckBase
|
public class FreeSpacePreCheck : PreCheckBase
|
||||||
{
|
{
|
||||||
private readonly InternalData _internalData;
|
private readonly InternalData _internalData;
|
||||||
|
|
||||||
public FreeSpacePreCheck(InternalData internalData) : base("Free Space", true)
|
public FreeSpacePreCheck(InternalData internalData) : base("Free Space", true)
|
||||||
{
|
{
|
||||||
_internalData = internalData;
|
_internalData = internalData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<PreCheckResult> CheckOperation()
|
public override async Task<PreCheckResult> CheckOperation()
|
||||||
{
|
{
|
||||||
if (_internalData.OriginalGamePath is null)
|
if (_internalData.OriginalGamePath is null)
|
||||||
return PreCheckResult.FromError("Could not find EFT game path");
|
return PreCheckResult.FromError("Could not find EFT game path");
|
||||||
|
|
||||||
if (_internalData.TargetInstallPath is null)
|
if (_internalData.TargetInstallPath is null)
|
||||||
return PreCheckResult.FromError("Could not find install target path");
|
return PreCheckResult.FromError("Could not find install target path");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var eftSourceDirectoryInfo = new DirectoryInfo(_internalData.OriginalGamePath);
|
var eftSourceDirectoryInfo = new DirectoryInfo(_internalData.OriginalGamePath);
|
||||||
var installTargetDirectoryInfo = new DirectoryInfo(_internalData.TargetInstallPath);
|
var installTargetDirectoryInfo = new DirectoryInfo(_internalData.TargetInstallPath);
|
||||||
|
|
||||||
|
var cacheDirectory = new DirectoryInfo(DownloadCacheHelper.CachePath);
|
||||||
|
|
||||||
var eftSourceDirSize = DirectorySizeHelper.GetSizeOfDirectory(eftSourceDirectoryInfo);
|
var eftSourceDirSize = DirectorySizeHelper.GetSizeOfDirectory(eftSourceDirectoryInfo);
|
||||||
|
|
||||||
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().FirstOrDefault(d => d.Name.ToLower() == installTargetDirectoryInfo.Root.Name.ToLower())?.AvailableFreeSpace ?? 0;
|
var availableSize = DriveInfo.GetDrives()
|
||||||
|
.FirstOrDefault(d => d.Name.ToLower() == installTargetDirectoryInfo.Root.Name.ToLower())
|
||||||
|
?.AvailableFreeSpace ?? 0;
|
||||||
|
|
||||||
// add 10Gb overhead to game files for potential patches / release files
|
// add 10Gb overhead to game files for potential patches / release files
|
||||||
eftSourceDirSize += 10000000000;
|
eftSourceDirSize += 10000000000;
|
||||||
|
|
||||||
var availableSpaceMessage = $"Available Space: {DirectorySizeHelper.SizeSuffix(availableSize, 2)}";
|
var availableSpaceMessage = $"Available Space: {DirectorySizeHelper.SizeSuffix(availableSize, 2)}";
|
||||||
var requiredSpaceMessage = $"Space Required for EFT Client: {DirectorySizeHelper.SizeSuffix(eftSourceDirSize, 2)} including ~10Gb overhead";
|
var requiredSpaceMessage =
|
||||||
|
$"Space Required for EFT Client: {DirectorySizeHelper.SizeSuffix(eftSourceDirSize, 2)} including ~10Gb overhead";
|
||||||
|
|
||||||
|
var cacheDriveMessage = "";
|
||||||
|
var cacheDriveOK = true;
|
||||||
|
|
||||||
|
// if cache directory is on another drive, check that drive for around 5Gb of required space
|
||||||
|
if (cacheDirectory.Root.Name.ToLower() != installTargetDirectoryInfo.Root.Name.ToLower())
|
||||||
|
{
|
||||||
|
cacheDriveOK = false;
|
||||||
|
var availableCacheDriveSize = DriveInfo.GetDrives()
|
||||||
|
.FirstOrDefault(d =>
|
||||||
|
d.Name.ToLower() == cacheDirectory.Root.Name.ToLower())
|
||||||
|
?.AvailableFreeSpace ??
|
||||||
|
0;
|
||||||
|
|
||||||
|
// check if the drive where the cache is has at least 5Gb of free space. We should only need 2-3Gb
|
||||||
|
if (availableCacheDriveSize > 5000000000)
|
||||||
|
{
|
||||||
|
cacheDriveMessage = $"Drive for cache '{cacheDirectory.Root.Name}' has at least 5Gb of space. Available: {DirectorySizeHelper.SizeSuffix(availableCacheDriveSize, 2)}";
|
||||||
|
cacheDriveOK = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cacheDriveMessage = $"Drive for cache '{cacheDirectory.Root.Name}' does NOT have at least 5Gb of space. Available: {DirectorySizeHelper.SizeSuffix(availableCacheDriveSize, 2)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (eftSourceDirSize > availableSize)
|
if (eftSourceDirSize > availableSize)
|
||||||
{
|
{
|
||||||
return PreCheckResult.FromError($"Not enough free space on {installTargetDirectoryInfo.Root.Name} to install SPT\n\n{availableSpaceMessage}\n{requiredSpaceMessage}");
|
return PreCheckResult.FromError(
|
||||||
|
$"Not enough free space on {installTargetDirectoryInfo.Root.Name} to install SPT\n\n{availableSpaceMessage}\n{requiredSpaceMessage}\n\n{cacheDriveMessage}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return PreCheckResult.FromSuccess($"There is enough space available on {installTargetDirectoryInfo.Root.Name} to install SPT.\n\n{availableSpaceMessage}\n{requiredSpaceMessage}");
|
var okGameSpaceMessage =
|
||||||
|
$"There is enough space available on {installTargetDirectoryInfo.Root.Name} to install SPT.\n\n{availableSpaceMessage}\n{requiredSpaceMessage}\n\n{cacheDriveMessage}";
|
||||||
|
|
||||||
|
if (!cacheDriveOK)
|
||||||
|
{
|
||||||
|
return PreCheckResult.FromError(okGameSpaceMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreCheckResult.FromSuccess(okGameSpaceMessage);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -12,14 +12,14 @@ public class Net8PreCheck : PreCheckBase
|
|||||||
public Net8PreCheck() : base(".Net 8 Desktop Runtime", true)
|
public Net8PreCheck() : base(".Net 8 Desktop Runtime", true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<PreCheckResult> CheckOperation()
|
public override async Task<PreCheckResult> CheckOperation()
|
||||||
{
|
{
|
||||||
var minRequiredVersion = new Version("8.0.0");
|
var minRequiredVersion = new Version("8.0.0");
|
||||||
string[] output;
|
string[] output;
|
||||||
|
|
||||||
var failedButtonText = "Download .Net 8 Desktop Runtime";
|
var failedButtonText = "Download .Net 8 Desktop Runtime";
|
||||||
|
|
||||||
var failedButtonAction = () =>
|
var failedButtonAction = () =>
|
||||||
{
|
{
|
||||||
Process.Start(new ProcessStartInfo
|
Process.Start(new ProcessStartInfo
|
||||||
@ -27,19 +27,26 @@ public class Net8PreCheck : PreCheckBase
|
|||||||
FileName = "cmd.exe",
|
FileName = "cmd.exe",
|
||||||
UseShellExecute = true,
|
UseShellExecute = true,
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
ArgumentList = { "/C", "start", "https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.2-windows-x64-installer" }
|
ArgumentList =
|
||||||
|
{
|
||||||
|
"/C", "start",
|
||||||
|
"https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.2-windows-x64-installer"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = ProcessHelper.RunAndReadProcessOutputs("dotnet", "--list-runtimes");
|
var programFiles = Environment.ExpandEnvironmentVariables("%ProgramW6432%");
|
||||||
|
var result =
|
||||||
|
ProcessHelper.RunAndReadProcessOutputs($@"{programFiles}\dotnet\dotnet.exe", "--list-runtimes");
|
||||||
|
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
return PreCheckResult.FromError(result.Message + "\n\nYou most likely don't have .net 8 installed", failedButtonText, failedButtonAction);
|
return PreCheckResult.FromError(result.Message + "\n\nYou most likely don't have .net 8 installed",
|
||||||
|
failedButtonText, failedButtonAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
output = result.StdOut.Split("\r\n");
|
output = result.StdOut.Split("\r\n");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -47,28 +54,31 @@ public class Net8PreCheck : PreCheckBase
|
|||||||
Log.Error(ex, $"PreCheck::{Name}::Exception");
|
Log.Error(ex, $"PreCheck::{Name}::Exception");
|
||||||
return PreCheckResult.FromException(ex);
|
return PreCheckResult.FromException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
var highestFoundVersion = new Version("0.0.0");
|
var highestFoundVersion = new Version("0.0.0");
|
||||||
|
|
||||||
foreach (var lineVersion in output)
|
foreach (var lineVersion in output)
|
||||||
{
|
{
|
||||||
var regex = Regex.Match(lineVersion, @"Microsoft.WindowsDesktop.App (\d\.\d\.\d)");
|
var regex = Regex.Match(lineVersion, @"Microsoft.WindowsDesktop.App (\d\.\d\.\d)");
|
||||||
|
|
||||||
if (!regex.Success || regex.Groups.Count < 1)
|
if (!regex.Success || regex.Groups.Count < 1)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var stringVersion = regex.Groups[1].Value;
|
var stringVersion = regex.Groups[1].Value;
|
||||||
|
|
||||||
var foundVersion = new Version(stringVersion);
|
var foundVersion = new Version(stringVersion);
|
||||||
|
|
||||||
if (foundVersion >= minRequiredVersion)
|
if (foundVersion >= minRequiredVersion)
|
||||||
{
|
{
|
||||||
return PreCheckResult.FromSuccess($".Net {minRequiredVersion} Desktop Runtime or higher is installed.\n\nInstalled Version: {foundVersion}");
|
return PreCheckResult.FromSuccess(
|
||||||
|
$".Net {minRequiredVersion} Desktop Runtime or higher is installed.\n\nInstalled Version: {foundVersion}");
|
||||||
}
|
}
|
||||||
|
|
||||||
highestFoundVersion = foundVersion > highestFoundVersion ? foundVersion : highestFoundVersion;
|
highestFoundVersion = foundVersion > highestFoundVersion ? foundVersion : highestFoundVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
return PreCheckResult.FromError($".Net Desktop Runtime version {minRequiredVersion} or higher is required.\n\nHighest Version Found: {(highestFoundVersion > new Version("0.0.0") ? highestFoundVersion : "Not Found")}\n\nThis is required to play SPT", failedButtonText, failedButtonAction);
|
return PreCheckResult.FromError(
|
||||||
|
$".Net Desktop Runtime version {minRequiredVersion} or higher is required.\n\nHighest Version Found: {(highestFoundVersion > new Version("0.0.0") ? highestFoundVersion : "Not Found")}\n\nThis is required to play SPT",
|
||||||
|
failedButtonText, failedButtonAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,19 +7,21 @@ using SPTInstaller.Helpers;
|
|||||||
|
|
||||||
namespace SPTInstaller.Installer_Tasks.PreChecks;
|
namespace SPTInstaller.Installer_Tasks.PreChecks;
|
||||||
|
|
||||||
|
[Obsolete(
|
||||||
|
"No longer needed, but keeping around for now just in case. Can be removed from code after 7/1/2024 if no issues are found")]
|
||||||
public class NetCore6PreCheck : PreCheckBase
|
public class NetCore6PreCheck : PreCheckBase
|
||||||
{
|
{
|
||||||
public NetCore6PreCheck() : base(".Net Core 6 Desktop Runtime", true)
|
public NetCore6PreCheck() : base(".Net Core 6 Desktop Runtime", true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<PreCheckResult> CheckOperation()
|
public override async Task<PreCheckResult> CheckOperation()
|
||||||
{
|
{
|
||||||
var minRequiredVersion = new Version("6.0.0");
|
var minRequiredVersion = new Version("6.0.0");
|
||||||
string[] output;
|
string[] output;
|
||||||
|
|
||||||
var failedButtonText = "Download .Net Core 6 Desktop Runtime";
|
var failedButtonText = "Download .Net Core 6 Desktop Runtime";
|
||||||
|
|
||||||
var failedButtonAction = () =>
|
var failedButtonAction = () =>
|
||||||
{
|
{
|
||||||
Process.Start(new ProcessStartInfo
|
Process.Start(new ProcessStartInfo
|
||||||
@ -27,19 +29,26 @@ public class NetCore6PreCheck : PreCheckBase
|
|||||||
FileName = "cmd.exe",
|
FileName = "cmd.exe",
|
||||||
UseShellExecute = true,
|
UseShellExecute = true,
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
ArgumentList = { "/C", "start", "https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-6.0.4-windows-x64-installer" }
|
ArgumentList =
|
||||||
|
{
|
||||||
|
"/C", "start",
|
||||||
|
"https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-6.0.4-windows-x64-installer"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = ProcessHelper.RunAndReadProcessOutputs("dotnet", "--list-runtimes");
|
var programFiles = Environment.ExpandEnvironmentVariables("%ProgramW6432%");
|
||||||
|
var result =
|
||||||
|
ProcessHelper.RunAndReadProcessOutputs($@"{programFiles}\dotnet\dotnet.exe", "--list-runtimes");
|
||||||
|
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
return PreCheckResult.FromError(result.Message + "\n\nYou most likely don't have .net 6 installed", failedButtonText, failedButtonAction);
|
return PreCheckResult.FromError(result.Message + "\n\nYou most likely don't have .net 6 installed",
|
||||||
|
failedButtonText, failedButtonAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
output = result.StdOut.Split("\r\n");
|
output = result.StdOut.Split("\r\n");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -47,28 +56,31 @@ public class NetCore6PreCheck : PreCheckBase
|
|||||||
Log.Error(ex, $"PreCheck::{Name}::Exception");
|
Log.Error(ex, $"PreCheck::{Name}::Exception");
|
||||||
return PreCheckResult.FromException(ex);
|
return PreCheckResult.FromException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
var highestFoundVersion = new Version("0.0.0");
|
var highestFoundVersion = new Version("0.0.0");
|
||||||
|
|
||||||
foreach (var lineVersion in output)
|
foreach (var lineVersion in output)
|
||||||
{
|
{
|
||||||
var regex = Regex.Match(lineVersion, @"Microsoft.WindowsDesktop.App (\d\.\d\.\d)");
|
var regex = Regex.Match(lineVersion, @"Microsoft.WindowsDesktop.App (\d\.\d\.\d)");
|
||||||
|
|
||||||
if (!regex.Success || regex.Groups.Count < 1)
|
if (!regex.Success || regex.Groups.Count < 1)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var stringVersion = regex.Groups[1].Value;
|
var stringVersion = regex.Groups[1].Value;
|
||||||
|
|
||||||
var foundVersion = new Version(stringVersion);
|
var foundVersion = new Version(stringVersion);
|
||||||
|
|
||||||
if (foundVersion >= minRequiredVersion)
|
if (foundVersion >= minRequiredVersion)
|
||||||
{
|
{
|
||||||
return PreCheckResult.FromSuccess($".Net Core {minRequiredVersion} Desktop Runtime or higher is installed.\n\nInstalled Version: {foundVersion}");
|
return PreCheckResult.FromSuccess(
|
||||||
|
$".Net Core {minRequiredVersion} Desktop Runtime or higher is installed.\n\nInstalled Version: {foundVersion}");
|
||||||
}
|
}
|
||||||
|
|
||||||
highestFoundVersion = foundVersion > highestFoundVersion ? foundVersion : highestFoundVersion;
|
highestFoundVersion = foundVersion > highestFoundVersion ? foundVersion : highestFoundVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
return PreCheckResult.FromError($".Net Core Desktop Runtime version {minRequiredVersion} or higher is required.\n\nHighest Version Found: {(highestFoundVersion > new Version("0.0.0") ? highestFoundVersion : "Not Found")}\n\nThis is required to play SPT, but you can install it later if and shouldn't affect the SPT install process.", failedButtonText, failedButtonAction);
|
return PreCheckResult.FromError(
|
||||||
|
$".Net Core Desktop Runtime version {minRequiredVersion} or higher is required.\n\nHighest Version Found: {(highestFoundVersion > new Version("0.0.0") ? highestFoundVersion : "Not Found")}\n\nThis is required to play SPT, but you can install it later if and shouldn't affect the SPT install process.",
|
||||||
|
failedButtonText, failedButtonAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,17 +11,17 @@ public class NetFramework472PreCheck : PreCheckBase
|
|||||||
public NetFramework472PreCheck() : base(".Net Framework 4.7.2", true)
|
public NetFramework472PreCheck() : base(".Net Framework 4.7.2", true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<PreCheckResult> CheckOperation()
|
public override async Task<PreCheckResult> CheckOperation()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var minRequiredVersion = new Version("4.7.2");
|
var minRequiredVersion = new Version("4.7.2");
|
||||||
|
|
||||||
var key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full");
|
var key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full");
|
||||||
|
|
||||||
var failedButtonText = "Download .Net Framework 4.7.2";
|
var failedButtonText = "Download .Net Framework 4.7.2";
|
||||||
|
|
||||||
var failedButtonAction = () =>
|
var failedButtonAction = () =>
|
||||||
{
|
{
|
||||||
Process.Start(new ProcessStartInfo
|
Process.Start(new ProcessStartInfo
|
||||||
@ -29,30 +29,40 @@ public class NetFramework472PreCheck : PreCheckBase
|
|||||||
FileName = "cmd.exe",
|
FileName = "cmd.exe",
|
||||||
UseShellExecute = true,
|
UseShellExecute = true,
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
ArgumentList = { "/C", "start", "https://dotnet.microsoft.com/download/dotnet-framework/thank-you/net472-developer-pack-offline-installer" }
|
ArgumentList =
|
||||||
|
{
|
||||||
|
"/C", "start",
|
||||||
|
"https://dotnet.microsoft.com/download/dotnet-framework/thank-you/net472-developer-pack-offline-installer"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (key == null)
|
if (key == null)
|
||||||
{
|
{
|
||||||
return PreCheckResult.FromError("Could not find .Net Framework on system.\n\nThis is required to play SPT, but you can install it later and shouldn't affect the SPT install process.", failedButtonText, failedButtonAction);
|
return PreCheckResult.FromError(
|
||||||
|
"Could not find .Net Framework on system.\n\nThis is required to play SPT, but you can install it later and shouldn't affect the SPT install process.",
|
||||||
|
failedButtonText, failedButtonAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = key.GetValue("Version");
|
var value = key.GetValue("Version");
|
||||||
|
|
||||||
if (value == null || value is not string versionString)
|
if (value == null || value is not string versionString)
|
||||||
{
|
{
|
||||||
return PreCheckResult.FromError("Something went wrong. This precheck failed for an unknown reason. :(");
|
return PreCheckResult.FromError(
|
||||||
|
"Something went wrong. This precheck failed for an unknown reason. :(");
|
||||||
}
|
}
|
||||||
|
|
||||||
var installedVersion = new Version(versionString);
|
var installedVersion = new Version(versionString);
|
||||||
|
|
||||||
if (installedVersion < minRequiredVersion)
|
if (installedVersion < minRequiredVersion)
|
||||||
{
|
{
|
||||||
return PreCheckResult.FromError($".Net Framework {versionString} is installed, but {minRequiredVersion} or higher is required.\n\nYou can install it later and shouldn't affect the SPT install process.", failedButtonText, failedButtonAction);
|
return PreCheckResult.FromError(
|
||||||
|
$".Net Framework {versionString} is installed, but {minRequiredVersion} or higher is required.\n\nYou can install it later and shouldn't affect the SPT install process.",
|
||||||
|
failedButtonText, failedButtonAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PreCheckResult.FromSuccess($".Net Framework {minRequiredVersion} or higher is installed.\n\nInstalled Version: {installedVersion}");
|
return PreCheckResult.FromSuccess(
|
||||||
|
$".Net Framework {minRequiredVersion} or higher is installed.\n\nInstalled Version: {installedVersion}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -3,20 +3,23 @@ using SPTInstaller.Models;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SPTInstaller.Installer_Tasks.PreChecks;
|
namespace SPTInstaller.Installer_Tasks.PreChecks;
|
||||||
|
|
||||||
public class TestPreCheck : PreCheckBase
|
public class TestPreCheck : PreCheckBase
|
||||||
{
|
{
|
||||||
private StatusSpinner.SpinnerState _endState;
|
private StatusSpinner.SpinnerState _endState;
|
||||||
public static TestPreCheck FromRandomName(StatusSpinner.SpinnerState EndState) => new TestPreCheck($"{EndState} #{new Random().Next(0, 9999)}", EndState == StatusSpinner.SpinnerState.Error, EndState);
|
|
||||||
|
public static TestPreCheck FromRandomName(StatusSpinner.SpinnerState EndState) => new TestPreCheck(
|
||||||
|
$"{EndState} #{new Random().Next(0, 9999)}", EndState == StatusSpinner.SpinnerState.Error, EndState);
|
||||||
|
|
||||||
public TestPreCheck(string name, bool isRequired, StatusSpinner.SpinnerState endState) : base(name, isRequired)
|
public TestPreCheck(string name, bool isRequired, StatusSpinner.SpinnerState endState) : base(name, isRequired)
|
||||||
{
|
{
|
||||||
_endState = endState;
|
_endState = endState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<PreCheckResult> CheckOperation()
|
public override async Task<PreCheckResult> CheckOperation()
|
||||||
{
|
{
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
|
|
||||||
switch (_endState)
|
switch (_endState)
|
||||||
{
|
{
|
||||||
case StatusSpinner.SpinnerState.Error:
|
case StatusSpinner.SpinnerState.Error:
|
||||||
@ -27,4 +30,4 @@ public class TestPreCheck : PreCheckBase
|
|||||||
return PreCheckResult.FromSuccess("This is what a successful precheck looks like");
|
return PreCheckResult.FromSuccess("This is what a successful precheck looks like");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,74 +1,103 @@
|
|||||||
using Gitea.Api;
|
using SPTInstaller.Interfaces;
|
||||||
using Gitea.Client;
|
|
||||||
using SPTInstaller.Interfaces;
|
|
||||||
using SPTInstaller.Models;
|
using SPTInstaller.Models;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using SPTInstaller.Helpers;
|
using SPTInstaller.Helpers;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using SPTInstaller.Models.Mirrors;
|
||||||
|
using SPTInstaller.Models.ReleaseInfo;
|
||||||
|
|
||||||
namespace SPTInstaller.Installer_Tasks;
|
namespace SPTInstaller.Installer_Tasks;
|
||||||
|
|
||||||
public class ReleaseCheckTask : InstallerTaskBase
|
public class ReleaseCheckTask : InstallerTaskBase
|
||||||
{
|
{
|
||||||
private InternalData _data;
|
private InternalData _data;
|
||||||
|
|
||||||
public ReleaseCheckTask(InternalData data) : base("Release Checks")
|
public ReleaseCheckTask(InternalData data) : base("Release Checks")
|
||||||
{
|
{
|
||||||
_data = data;
|
_data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<IResult> TaskOperation()
|
public override async Task<IResult> TaskOperation()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var repo = new RepositoryApi(Configuration.Default);
|
|
||||||
|
|
||||||
SetStatus("Checking SPT Releases", "", null, ProgressStyle.Indeterminate);
|
SetStatus("Checking SPT Releases", "", null, ProgressStyle.Indeterminate);
|
||||||
|
|
||||||
var akiRepoReleases = await repo.RepoListReleasesAsync("SPT-AKI", "Stable-releases");
|
var progress = new Progress<double>((d) => { SetStatus(null, null, (int)Math.Floor(d)); });
|
||||||
|
var SPTReleaseInfoFile =
|
||||||
|
await DownloadCacheHelper.GetOrDownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
|
||||||
|
progress, DownloadCacheHelper.SuggestedTtl);
|
||||||
|
|
||||||
|
if (SPTReleaseInfoFile == null)
|
||||||
|
{
|
||||||
|
return Result.FromError("Failed to download release metadata, try clicking the 'Whats this' button below followed by the 'Clear Metadata cache' button");
|
||||||
|
}
|
||||||
|
|
||||||
|
var SPTReleaseInfo =
|
||||||
|
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(SPTReleaseInfoFile.FullName));
|
||||||
|
|
||||||
SetStatus("Checking for Patches", "", null, ProgressStyle.Indeterminate);
|
SetStatus("Checking for Patches", "", null, ProgressStyle.Indeterminate);
|
||||||
|
|
||||||
var patchRepoReleases = await repo.RepoListReleasesAsync("SPT-AKI", "Downgrade-Patches");
|
var SPTPatchMirrorsFile =
|
||||||
|
await DownloadCacheHelper.GetOrDownloadFileAsync("mirrors.json", DownloadCacheHelper.PatchMirrorUrl,
|
||||||
var latestAkiRelease = akiRepoReleases.FindAll(x => !x.Prerelease)[0];
|
progress, DownloadCacheHelper.SuggestedTtl);
|
||||||
var latestAkiVersion = latestAkiRelease.Name.Replace('(', ' ').Replace(')', ' ').Split(' ')[3];
|
|
||||||
var comparePatchToAki = patchRepoReleases?.Find(x => x.Name.Contains(_data.OriginalGameVersion) && x.Name.Contains(latestAkiVersion));
|
if (SPTPatchMirrorsFile == null)
|
||||||
|
|
||||||
_data.PatcherMirrorsLink = comparePatchToAki?.Assets[0].BrowserDownloadUrl;
|
|
||||||
_data.AkiReleaseDownloadLink = latestAkiRelease.Assets[0].BrowserDownloadUrl;
|
|
||||||
_data.AkiReleaseHash = FileHashHelper.GetGiteaReleaseHash(latestAkiRelease);
|
|
||||||
|
|
||||||
int IntAkiVersion = int.Parse(latestAkiVersion);
|
|
||||||
int IntGameVersion = int.Parse(_data.OriginalGameVersion);
|
|
||||||
bool patchNeedCheck = false;
|
|
||||||
|
|
||||||
if (IntGameVersion > IntAkiVersion)
|
|
||||||
{
|
{
|
||||||
patchNeedCheck = true;
|
return Result.FromError("Failed to download patch mirror data, try clicking the 'Whats this' button below followed by the 'Clear Metadata cache' button");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IntGameVersion < IntAkiVersion)
|
var patchMirrorInfo =
|
||||||
|
JsonConvert.DeserializeObject<PatchInfo>(File.ReadAllText(SPTPatchMirrorsFile.FullName));
|
||||||
|
|
||||||
|
if (SPTReleaseInfo == null || patchMirrorInfo == null)
|
||||||
{
|
{
|
||||||
return Result.FromError("Your client is outdated. Please update EFT");
|
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");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IntGameVersion == IntAkiVersion)
|
_data.ReleaseInfo = SPTReleaseInfo;
|
||||||
|
_data.PatchInfo = patchMirrorInfo;
|
||||||
|
int intSPTVersion = int.Parse(SPTReleaseInfo.ClientVersion);
|
||||||
|
int intGameVersion = int.Parse(_data.OriginalGameVersion);
|
||||||
|
|
||||||
|
// note: it's possible the game version could be lower than the SPT version and still need a patch if the major version numbers change
|
||||||
|
// : it's probably a low chance though
|
||||||
|
bool patchNeedCheck = intGameVersion > intSPTVersion;
|
||||||
|
|
||||||
|
if (intGameVersion < intSPTVersion)
|
||||||
|
{
|
||||||
|
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 == intSPTVersion)
|
||||||
{
|
{
|
||||||
patchNeedCheck = false;
|
patchNeedCheck = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comparePatchToAki == null && patchNeedCheck)
|
bool sptClientIsOutdated = intSPTVersion != patchMirrorInfo.TargetClientVersion && patchNeedCheck;
|
||||||
|
bool liveClientIsOutdated = intGameVersion != patchMirrorInfo.SourceClientVersion && patchNeedCheck;
|
||||||
|
|
||||||
|
if (sptClientIsOutdated)
|
||||||
{
|
{
|
||||||
return Result.FromError("No patcher available for your version");
|
return Result.FromError(
|
||||||
|
"Could not find a downgrade patcher for the version of EFT you have installed." +
|
||||||
|
"\nThis can happen due to one of the following reasons:" +
|
||||||
|
"\n* Live EFT just updated. The SPT team will create a new patcher within 24 hours, hold tight!" +
|
||||||
|
"\n* Live EFT just updated. You have not installed it on your computer using your Battlestate Games launcher");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (liveClientIsOutdated)
|
||||||
|
{
|
||||||
|
return Result.FromError("Your live EFT is out of date. Please update it using your Battlestate Games Launcher then run the SPT Installer again");
|
||||||
|
}
|
||||||
|
|
||||||
_data.PatchNeeded = patchNeedCheck;
|
_data.PatchNeeded = patchNeedCheck;
|
||||||
|
|
||||||
string status = $"Current Release: {latestAkiVersion} - {(_data.PatchNeeded ? "Patch Available" : "No Patch Needed")}";
|
string status =
|
||||||
|
$"Current Release: {SPTReleaseInfo.ClientVersion} - {(_data.PatchNeeded ? "Patch Available" : "No Patch Needed")}";
|
||||||
|
|
||||||
SetStatus(null, status);
|
SetStatus(null, status);
|
||||||
|
|
||||||
return Result.FromSuccess(status);
|
return Result.FromSuccess(status);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -9,84 +9,84 @@ namespace SPTInstaller.Installer_Tasks;
|
|||||||
public class SetupClientTask : InstallerTaskBase
|
public class SetupClientTask : InstallerTaskBase
|
||||||
{
|
{
|
||||||
private InternalData _data;
|
private InternalData _data;
|
||||||
|
|
||||||
public SetupClientTask(InternalData data) : base("Setup Client")
|
public SetupClientTask(InternalData data) : base("Setup Client")
|
||||||
{
|
{
|
||||||
_data = data;
|
_data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<IResult> TaskOperation()
|
public override async Task<IResult> TaskOperation()
|
||||||
{
|
{
|
||||||
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
|
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
|
||||||
|
|
||||||
var patcherOutputDir = new DirectoryInfo(Path.Join(_data.TargetInstallPath, "patcher"));
|
var patcherOutputDir = new DirectoryInfo(Path.Join(_data.TargetInstallPath, "patcher"));
|
||||||
|
|
||||||
var patcherEXE = new FileInfo(Path.Join(_data.TargetInstallPath, "patcher.exe"));
|
var patcherEXE = new FileInfo(Path.Join(_data.TargetInstallPath, "patcher.exe"));
|
||||||
|
|
||||||
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)); });
|
||||||
|
|
||||||
|
SetStatus("Preparing 7z", "", null, ProgressStyle.Indeterminate);
|
||||||
|
|
||||||
|
if (!FileHelper.StreamAssemblyResourceOut("7z.dll", Path.Join(DownloadCacheHelper.CachePath, "7z.dll")))
|
||||||
|
{
|
||||||
|
return Result.FromError("Failed to prepare 7z");
|
||||||
|
}
|
||||||
|
|
||||||
if (_data.PatchNeeded)
|
if (_data.PatchNeeded)
|
||||||
{
|
{
|
||||||
SetStatus("Preparing 7z", "", null, ProgressStyle.Indeterminate);
|
|
||||||
|
|
||||||
if (!FileHelper.StreamAssemblyResourceOut("7z.dll", Path.Join(DownloadCacheHelper.CachePath, "7z.dll")))
|
|
||||||
{
|
|
||||||
return Result.FromError("Failed to prepare 7z");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
if (!extractPatcherResult.Succeeded)
|
if (!extractPatcherResult.Succeeded)
|
||||||
{
|
{
|
||||||
return extractPatcherResult;
|
return extractPatcherResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy patcher files to install directory
|
// copy patcher files to install directory
|
||||||
SetStatus("Copying Patcher", "", 0);
|
SetStatus("Copying Patcher", "", 0);
|
||||||
|
|
||||||
var patcherDirInfo = patcherOutputDir.GetDirectories("Patcher*", SearchOption.TopDirectoryOnly).First();
|
var patcherDirInfo = patcherOutputDir.GetDirectories("Patcher*", SearchOption.TopDirectoryOnly).First();
|
||||||
|
|
||||||
var copyPatcherResult = FileHelper.CopyDirectoryWithProgress(patcherDirInfo, targetInstallDirInfo, progress);
|
var copyPatcherResult =
|
||||||
|
FileHelper.CopyDirectoryWithProgress(patcherDirInfo, targetInstallDirInfo, progress);
|
||||||
|
|
||||||
if (!copyPatcherResult.Succeeded)
|
if (!copyPatcherResult.Succeeded)
|
||||||
{
|
{
|
||||||
return copyPatcherResult;
|
return copyPatcherResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// run patcher
|
// run patcher
|
||||||
SetStatus("Running Patcher", "", null, ProgressStyle.Indeterminate);
|
SetStatus("Running Patcher", "", null, ProgressStyle.Indeterminate);
|
||||||
|
|
||||||
var patchingResult = ProcessHelper.PatchClientFiles(patcherEXE, targetInstallDirInfo);
|
var patchingResult = ProcessHelper.PatchClientFiles(patcherEXE, targetInstallDirInfo);
|
||||||
|
|
||||||
if (!patchingResult.Succeeded)
|
if (!patchingResult.Succeeded)
|
||||||
{
|
{
|
||||||
return patchingResult;
|
return patchingResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
{
|
{
|
||||||
return extractReleaseResult;
|
return extractReleaseResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup temp files
|
// cleanup temp files
|
||||||
SetStatus("Cleanup", "almost done :)", null, ProgressStyle.Indeterminate);
|
SetStatus("Cleanup", "almost done :)", null, ProgressStyle.Indeterminate);
|
||||||
|
|
||||||
if(_data.PatchNeeded)
|
if (_data.PatchNeeded)
|
||||||
{
|
{
|
||||||
patcherOutputDir.Delete(true);
|
patcherOutputDir.Delete(true);
|
||||||
patcherEXE.Delete();
|
patcherEXE.Delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.FromSuccess("SPT is Setup. Happy Playing!");
|
return Result.FromSuccess("SPT is Setup. Happy Playing!");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,27 +7,27 @@ namespace SPTInstaller.Installer_Tasks;
|
|||||||
internal class TestTask : InstallerTaskBase
|
internal class TestTask : InstallerTaskBase
|
||||||
{
|
{
|
||||||
public static TestTask FromRandomName() => new TestTask($"Test Task #{new Random().Next(0, 9999)}");
|
public static TestTask FromRandomName() => new TestTask($"Test Task #{new Random().Next(0, 9999)}");
|
||||||
|
|
||||||
public TestTask(string name) : base(name)
|
public TestTask(string name) : base(name)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async override Task<IResult> TaskOperation()
|
public async override Task<IResult> TaskOperation()
|
||||||
{
|
{
|
||||||
var total = 4;
|
var total = 4;
|
||||||
var interval = TimeSpan.FromSeconds(1);
|
var interval = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
for(var i = 0; i < total; i++)
|
for (var i = 0; i < total; i++)
|
||||||
{
|
{
|
||||||
var count = i + 1;
|
var count = i + 1;
|
||||||
var progressMessage = $"Running Task: {Name}";
|
var progressMessage = $"Running Task: {Name}";
|
||||||
var progress = (int)Math.Floor((double)count / total * 100);
|
var progress = (int)Math.Floor((double)count / total * 100);
|
||||||
|
|
||||||
SetStatus(progressMessage, $"Details: ({count}/{total})", progress);
|
SetStatus(progressMessage, $"Details: ({count}/{total})", progress);
|
||||||
|
|
||||||
await Task.Delay(interval);
|
await Task.Delay(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.FromSuccess();
|
return Result.FromSuccess();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,8 +2,9 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SPTInstaller.Interfaces;
|
namespace SPTInstaller.Interfaces;
|
||||||
|
|
||||||
public interface IMirrorDownloader
|
public interface IMirrorDownloader
|
||||||
{
|
{
|
||||||
public DownloadMirror MirrorInfo { get; }
|
public PatchInfoMirror MirrorInfo { get; }
|
||||||
public Task<FileInfo?> Download(IProgress<double> progress);
|
public Task<FileInfo?> Download(IProgress<double> progress);
|
||||||
}
|
}
|
@ -11,6 +11,6 @@ public interface IPreCheck
|
|||||||
public string PreCheckDetails { get; }
|
public string PreCheckDetails { get; }
|
||||||
public StatusSpinner.SpinnerState State { get; set; }
|
public StatusSpinner.SpinnerState State { get; set; }
|
||||||
public event EventHandler ReeevaluationRequested;
|
public event EventHandler ReeevaluationRequested;
|
||||||
|
|
||||||
public Task<IResult> RunCheck();
|
public Task<IResult> RunCheck();
|
||||||
}
|
}
|
@ -6,18 +6,18 @@ public interface IProgressableTask
|
|||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public bool IsCompleted { get; }
|
public bool IsCompleted { get; }
|
||||||
|
|
||||||
public bool HasErrors { get; }
|
public bool HasErrors { get; }
|
||||||
|
|
||||||
public bool IsRunning { get; }
|
public bool IsRunning { get; }
|
||||||
|
|
||||||
public string StatusMessage { get; }
|
public string StatusMessage { get; }
|
||||||
|
|
||||||
public int Progress { get; }
|
public int Progress { get; }
|
||||||
|
|
||||||
public bool ShowProgress { get; }
|
public bool ShowProgress { get; }
|
||||||
|
|
||||||
public Task<IResult> RunAsync();
|
public Task<IResult> RunAsync();
|
||||||
}
|
}
|
24
SPTInstaller/Models/CopyInfo.cs
Normal file
24
SPTInstaller/Models/CopyInfo.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
SPTInstaller/Models/InstallerInfo.cs
Normal file
9
SPTInstaller/Models/InstallerInfo.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace SPTInstaller.Models;
|
||||||
|
|
||||||
|
public class InstallerInfo
|
||||||
|
{
|
||||||
|
public string LatestVersion { get; set; }
|
||||||
|
public List<string> Changes { get; set; }
|
||||||
|
}
|
@ -8,82 +8,92 @@ namespace SPTInstaller.Models;
|
|||||||
public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
|
public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
|
||||||
{
|
{
|
||||||
private string _id;
|
private string _id;
|
||||||
|
|
||||||
public string Id
|
public string Id
|
||||||
{
|
{
|
||||||
get => _id;
|
get => _id;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _id, value);
|
private set => this.RaiseAndSetIfChanged(ref _id, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _name;
|
private string _name;
|
||||||
|
|
||||||
public string Name
|
public string Name
|
||||||
{
|
{
|
||||||
get => _name;
|
get => _name;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _name, value);
|
private set => this.RaiseAndSetIfChanged(ref _name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _isComleted;
|
private bool _isComleted;
|
||||||
|
|
||||||
public bool IsCompleted
|
public bool IsCompleted
|
||||||
{
|
{
|
||||||
get => _isComleted;
|
get => _isComleted;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _isComleted, value);
|
private set => this.RaiseAndSetIfChanged(ref _isComleted, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _hasErrors;
|
private bool _hasErrors;
|
||||||
|
|
||||||
public bool HasErrors
|
public bool HasErrors
|
||||||
{
|
{
|
||||||
get => _hasErrors;
|
get => _hasErrors;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _hasErrors, value);
|
private set => this.RaiseAndSetIfChanged(ref _hasErrors, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _isRunning;
|
private bool _isRunning;
|
||||||
|
|
||||||
public bool IsRunning
|
public bool IsRunning
|
||||||
{
|
{
|
||||||
get => _isRunning;
|
get => _isRunning;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _isRunning, value);
|
private set => this.RaiseAndSetIfChanged(ref _isRunning, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int _progress;
|
private int _progress;
|
||||||
|
|
||||||
public int Progress
|
public int Progress
|
||||||
{
|
{
|
||||||
get => _progress;
|
get => _progress;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _progress, value);
|
private set => this.RaiseAndSetIfChanged(ref _progress, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _showProgress;
|
private bool _showProgress;
|
||||||
|
|
||||||
public bool ShowProgress
|
public bool ShowProgress
|
||||||
{
|
{
|
||||||
get => _showProgress;
|
get => _showProgress;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _showProgress, value);
|
private set => this.RaiseAndSetIfChanged(ref _showProgress, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _indeterminateProgress;
|
private bool _indeterminateProgress;
|
||||||
|
|
||||||
public bool IndeterminateProgress
|
public bool IndeterminateProgress
|
||||||
{
|
{
|
||||||
get => _indeterminateProgress;
|
get => _indeterminateProgress;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _indeterminateProgress, value);
|
private set => this.RaiseAndSetIfChanged(ref _indeterminateProgress, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _statusMessage;
|
private string _statusMessage;
|
||||||
|
|
||||||
public string StatusMessage
|
public string StatusMessage
|
||||||
{
|
{
|
||||||
get => _statusMessage;
|
get => _statusMessage;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
|
private set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _statusDetails;
|
private string _statusDetails;
|
||||||
|
|
||||||
public string StatusDetails
|
public string StatusDetails
|
||||||
{
|
{
|
||||||
get => _statusDetails;
|
get => _statusDetails;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _statusDetails, value);
|
private set => this.RaiseAndSetIfChanged(ref _statusDetails, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ProgressStyle
|
public enum ProgressStyle
|
||||||
{
|
{
|
||||||
Hidden = 0,
|
Hidden = 0,
|
||||||
Shown,
|
Shown,
|
||||||
Indeterminate,
|
Indeterminate,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the status details of the task
|
/// Update the status details of the task
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -91,28 +101,29 @@ public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
|
|||||||
/// <param name="details">The details of the task. Not updated if null</param>
|
/// <param name="details">The details of the task. Not updated if null</param>
|
||||||
/// <param name="progress">Progress of the task. Overrides progressStyle if a non-null value is supplied</param>
|
/// <param name="progress">Progress of the task. Overrides progressStyle if a non-null value is supplied</param>
|
||||||
/// <param name="progressStyle">The style of the progress bar</param>
|
/// <param name="progressStyle">The style of the progress bar</param>
|
||||||
public void SetStatus(string? message, string? details, int? progress = null, ProgressStyle? progressStyle = null, bool noLog = false)
|
public void SetStatus(string? message, string? details, int? progress = null, ProgressStyle? progressStyle = null,
|
||||||
|
bool noLog = false)
|
||||||
{
|
{
|
||||||
if(message != null && message != StatusMessage)
|
if (message != null && message != StatusMessage)
|
||||||
{
|
{
|
||||||
if (!noLog && !string.IsNullOrWhiteSpace(message))
|
if (!noLog && !string.IsNullOrWhiteSpace(message))
|
||||||
{
|
{
|
||||||
Log.Information($" <===> {message} <===>");
|
Log.Information($" <===> {message} <===>");
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusMessage = message;
|
StatusMessage = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(details != null && details != StatusDetails)
|
if (details != null && details != StatusDetails)
|
||||||
{
|
{
|
||||||
if (!noLog && !string.IsNullOrWhiteSpace(details))
|
if (!noLog && !string.IsNullOrWhiteSpace(details))
|
||||||
{
|
{
|
||||||
Log.Information(details);
|
Log.Information(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusDetails = details;
|
StatusDetails = details;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressStyle != null)
|
if (progressStyle != null)
|
||||||
{
|
{
|
||||||
switch (progressStyle)
|
switch (progressStyle)
|
||||||
@ -131,7 +142,7 @@ public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progress != null)
|
if (progress != null)
|
||||||
{
|
{
|
||||||
ShowProgress = true;
|
ShowProgress = true;
|
||||||
@ -139,13 +150,13 @@ public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
|
|||||||
Progress = progress.Value;
|
Progress = progress.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public InstallerTaskBase(string name)
|
public InstallerTaskBase(string name)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Id = Guid.NewGuid().ToString();
|
Id = Guid.NewGuid().ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A method for the install controller to call. Do not use this within your task
|
/// A method for the install controller to call. Do not use this within your task
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -153,23 +164,23 @@ public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
|
|||||||
public async Task<IResult> RunAsync()
|
public async Task<IResult> RunAsync()
|
||||||
{
|
{
|
||||||
IsRunning = true;
|
IsRunning = true;
|
||||||
|
|
||||||
var result = await TaskOperation();
|
var result = await TaskOperation();
|
||||||
|
|
||||||
IsRunning = false;
|
IsRunning = false;
|
||||||
|
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
HasErrors = true;
|
HasErrors = true;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
IsCompleted = true;
|
IsCompleted = true;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The task you want to run
|
/// The task you want to run
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,109 +1,124 @@
|
|||||||
using Gitea.Api;
|
using ReactiveUI;
|
||||||
using Gitea.Client;
|
|
||||||
using ReactiveUI;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using SPTInstaller.Helpers;
|
using SPTInstaller.Helpers;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace SPTInstaller.Models;
|
namespace SPTInstaller.Models;
|
||||||
|
|
||||||
public class InstallerUpdateInfo : ReactiveObject
|
public class InstallerUpdateInfo : ReactiveObject
|
||||||
{
|
{
|
||||||
private Version? _newVersion;
|
private Version? _newVersion;
|
||||||
|
|
||||||
public string NewInstallerUrl = "";
|
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 = "";
|
||||||
|
|
||||||
public string UpdateInfoText
|
public string UpdateInfoText
|
||||||
{
|
{
|
||||||
get => _updateInfoText;
|
get => _updateInfoText;
|
||||||
set => this.RaiseAndSetIfChanged(ref _updateInfoText, value);
|
set => this.RaiseAndSetIfChanged(ref _updateInfoText, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _showCard = false;
|
|
||||||
public bool ShowCard
|
|
||||||
{
|
|
||||||
get => _showCard;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _showCard, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _updating = false;
|
private bool _updating = false;
|
||||||
|
|
||||||
public bool Updating
|
public bool Updating
|
||||||
{
|
{
|
||||||
get => _updating;
|
get => _updating;
|
||||||
set => this.RaiseAndSetIfChanged(ref _updating, value);
|
set => this.RaiseAndSetIfChanged(ref _updating, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _updateAvailable = false;
|
private bool _updateAvailable = false;
|
||||||
|
|
||||||
public bool UpdateAvailable
|
public bool UpdateAvailable
|
||||||
{
|
{
|
||||||
get => _updateAvailable;
|
get => _updateAvailable;
|
||||||
set => this.RaiseAndSetIfChanged(ref _updateAvailable, value);
|
set => this.RaiseAndSetIfChanged(ref _updateAvailable, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _checkingForUpdates = false;
|
private bool _checkingForUpdates = false;
|
||||||
|
|
||||||
public bool CheckingForUpdates
|
public bool CheckingForUpdates
|
||||||
{
|
{
|
||||||
get => _checkingForUpdates;
|
get => _checkingForUpdates;
|
||||||
set => this.RaiseAndSetIfChanged(ref _checkingForUpdates, value);
|
set => this.RaiseAndSetIfChanged(ref _checkingForUpdates, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int _downloadProgress;
|
private int _downloadProgress;
|
||||||
|
|
||||||
public int DownloadProgress
|
public int DownloadProgress
|
||||||
{
|
{
|
||||||
get => _downloadProgress;
|
get => _downloadProgress;
|
||||||
set => this.RaiseAndSetIfChanged(ref _downloadProgress, value);
|
set => this.RaiseAndSetIfChanged(ref _downloadProgress, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateInstaller()
|
public async Task UpdateInstaller()
|
||||||
{
|
{
|
||||||
Updating = true;
|
Updating = true;
|
||||||
UpdateAvailable = false;
|
UpdateAvailable = false;
|
||||||
|
|
||||||
var updater = new FileInfo(Path.Join(DownloadCacheHelper.CachePath, "update.ps1"));
|
var updater = new FileInfo(Path.Join(DownloadCacheHelper.CachePath, "update.ps1"));
|
||||||
|
|
||||||
if (!FileHelper.StreamAssemblyResourceOut("update.ps1", updater.FullName))
|
if (!FileHelper.StreamAssemblyResourceOut("update.ps1", updater.FullName))
|
||||||
{
|
{
|
||||||
Log.Fatal("Failed to prepare update file");
|
Log.Fatal("Failed to prepare update file");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!updater.Exists)
|
if (!updater.Exists)
|
||||||
{
|
{
|
||||||
UpdateInfoText = "Failed to get updater from resources :(";
|
UpdateInfoText = "Failed to get updater from resources :(";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var newInstallerPath = await DownloadNewInstaller();
|
var newInstallerPath = await DownloadNewInstaller();
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(newInstallerPath))
|
if (string.IsNullOrWhiteSpace(newInstallerPath))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Process.Start(new ProcessStartInfo
|
Process.Start(new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = "powershell.exe",
|
FileName = "powershell.exe",
|
||||||
ArgumentList = { "-ExecutionPolicy", "Bypass", "-File", $"{updater.FullName}", $"{newInstallerPath}", $"{Path.Join(Environment.CurrentDirectory, "SPTInstaller.exe")}" }
|
ArgumentList =
|
||||||
|
{
|
||||||
|
"-ExecutionPolicy", "Bypass", "-File", $"{updater.FullName}", $"{newInstallerPath}",
|
||||||
|
$"{Path.Join(Environment.CurrentDirectory, "SPTInstaller.exe")}"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> DownloadNewInstaller()
|
private async Task<string> DownloadNewInstaller()
|
||||||
{
|
{
|
||||||
UpdateInfoText = $"Downloading new installer v{_newVersion}";
|
UpdateInfoText = $"Downloading installer v{NewVersion}";
|
||||||
|
|
||||||
var progress = new Progress<double>(x => DownloadProgress = (int)x);
|
var progress = new Progress<double>(x => DownloadProgress = (int)x);
|
||||||
|
|
||||||
var file = await DownloadCacheHelper.DownloadFileAsync("SPTInstller.exe", NewInstallerUrl, progress);
|
var file = await DownloadCacheHelper.DownloadFileAsync("SPTInstaller.exe", DownloadCacheHelper.InstallerUrl,
|
||||||
|
progress);
|
||||||
|
|
||||||
if (file == null || !file.Exists)
|
if (file == null || !file.Exists)
|
||||||
{
|
{
|
||||||
UpdateInfoText = "Failed to download new installer :(";
|
UpdateInfoText = "Failed to download new installer :(";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return file.FullName;
|
return file.FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EndCheck(string infoText, bool updateAvailable, bool log = true)
|
private void EndCheck(string infoText, bool updateAvailable, bool log = true)
|
||||||
{
|
{
|
||||||
if (log)
|
if (log)
|
||||||
@ -112,68 +127,56 @@ public class InstallerUpdateInfo : ReactiveObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
UpdateInfoText = infoText;
|
UpdateInfoText = infoText;
|
||||||
|
|
||||||
if (!updateAvailable)
|
|
||||||
{
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
// delay card dismiss
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
|
||||||
ShowCard = updateAvailable;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ShowCard = updateAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckingForUpdates = false;
|
CheckingForUpdates = false;
|
||||||
UpdateAvailable = updateAvailable;
|
UpdateAvailable = updateAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CheckForUpdates(Version? currentVersion)
|
public async Task CheckForUpdates(Version? currentVersion)
|
||||||
{
|
{
|
||||||
if (currentVersion == null)
|
if (currentVersion == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
UpdateInfoText = "Checking for installer updates";
|
UpdateInfoText = "Checking for installer updates";
|
||||||
ShowCard = true;
|
|
||||||
CheckingForUpdates = true;
|
CheckingForUpdates = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var repo = new RepositoryApi(Configuration.Default);
|
var installerInfoFile =
|
||||||
|
await DownloadCacheHelper.GetOrDownloadFileAsync("installer.json", DownloadCacheHelper.InstallerInfoUrl, null
|
||||||
var releases = await repo.RepoListReleasesAsync("CWX", "SPT-AKI-Installer");
|
, DownloadCacheHelper.SuggestedTtl);
|
||||||
|
|
||||||
if (releases == null || releases.Count == 0)
|
if (installerInfoFile == null)
|
||||||
{
|
{
|
||||||
EndCheck("No releases available", false);
|
EndCheck("Failed to download installer info", false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var latest = releases.FindAll(x => !x.Prerelease)[0];
|
var installerInfo =
|
||||||
|
JsonConvert.DeserializeObject<InstallerInfo>(File.ReadAllText(installerInfoFile.FullName));
|
||||||
if (latest == null)
|
|
||||||
|
if (installerInfo == null)
|
||||||
{
|
{
|
||||||
EndCheck("could not get latest release", false);
|
EndCheck("Failed to parse installer info json", false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var latestVersion = new Version(latest.TagName);
|
var latestVersion = new Version(installerInfo.LatestVersion);
|
||||||
|
|
||||||
if (latestVersion == null || latestVersion <= currentVersion)
|
if (latestVersion <= currentVersion)
|
||||||
{
|
{
|
||||||
EndCheck("No updates available", false);
|
EndCheck("No updates available", false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_newVersion = latestVersion;
|
NewVersion = latestVersion;
|
||||||
|
|
||||||
NewInstallerUrl = latest.Assets[0].BrowserDownloadUrl;
|
foreach (var change in installerInfo.Changes)
|
||||||
|
{
|
||||||
EndCheck($"Update available, version {latestVersion}", true);
|
ChangeLog += $"◉ {change}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
EndCheck($"Update Installer: v{latestVersion}", true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -181,7 +184,7 @@ public class InstallerUpdateInfo : ReactiveObject
|
|||||||
EndCheck(ex.Message, false, false);
|
EndCheck(ex.Message, false, false);
|
||||||
Log.Error(ex, "Failed to check for updates");
|
Log.Error(ex, "Failed to check for updates");
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,50 +1,48 @@
|
|||||||
using System.Collections.Generic;
|
using SPTInstaller.Models.Mirrors;
|
||||||
using SPTInstaller.Models.Mirrors;
|
using SPTInstaller.Models.ReleaseInfo;
|
||||||
|
|
||||||
namespace SPTInstaller.Models;
|
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>
|
||||||
public string? TargetInstallPath { get; set; }
|
public string? TargetInstallPath { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The orginal EFT game path
|
/// The orginal EFT game path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? OriginalGamePath { get; set; }
|
public string? OriginalGamePath { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The original EFT game version
|
/// The original EFT game version
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string OriginalGameVersion { get; set; }
|
public string OriginalGameVersion { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Patcher zip file info
|
/// Patcher zip file info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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 download link for SPT-AKI
|
/// The release information from release.json
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AkiReleaseDownloadLink { get; set; }
|
public ReleaseInfo.ReleaseInfo ReleaseInfo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public PatchInfo PatchInfo { get; set; }
|
||||||
/// The release zip hash
|
|
||||||
/// </summary>
|
|
||||||
public string AkiReleaseHash { get; set; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The release download link for the patcher mirror list
|
/// The release download link for the patcher mirror list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string PatcherMirrorsLink { get; set; }
|
// public string PatcherMirrorsLink { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not a patch is needed to downgrade the client files
|
/// Whether or not a patch is needed to downgrade the client files
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
38
SPTInstaller/Models/KnownFolders.cs
Normal file
38
SPTInstaller/Models/KnownFolders.cs
Normal 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);
|
||||||
|
}
|
@ -2,19 +2,20 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SPTInstaller.Models.Mirrors.Downloaders;
|
namespace SPTInstaller.Models.Mirrors.Downloaders;
|
||||||
|
|
||||||
public class HttpMirrorDownloader : MirrorDownloaderBase
|
public class HttpMirrorDownloader : MirrorDownloaderBase
|
||||||
{
|
{
|
||||||
public HttpMirrorDownloader(DownloadMirror mirror) : base(mirror)
|
public HttpMirrorDownloader(PatchInfoMirror mirror) : base(mirror)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<FileInfo?> Download(IProgress<double> progress)
|
public override async Task<FileInfo?> Download(IProgress<double> progress)
|
||||||
{
|
{
|
||||||
var file = await DownloadCacheHelper.DownloadFileAsync("patcher", MirrorInfo.Link, progress);
|
var file = await DownloadCacheHelper.DownloadFileAsync("patcher", MirrorInfo.Link, progress);
|
||||||
|
|
||||||
if (file == null)
|
if (file == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return FileHashHelper.CheckHash(file, MirrorInfo.Hash) ? file : null;
|
return FileHashHelper.CheckHash(file, MirrorInfo.Hash) ? file : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,40 +4,46 @@ using System.Threading.Tasks;
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace SPTInstaller.Models.Mirrors.Downloaders;
|
namespace SPTInstaller.Models.Mirrors.Downloaders;
|
||||||
|
|
||||||
public class MegaMirrorDownloader : MirrorDownloaderBase
|
public class MegaMirrorDownloader : MirrorDownloaderBase
|
||||||
{
|
{
|
||||||
public MegaMirrorDownloader(DownloadMirror mirrorInfo) : base(mirrorInfo)
|
public MegaMirrorDownloader(PatchInfoMirror mirrorInfo) : base(mirrorInfo)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<FileInfo?> Download(IProgress<double> progress)
|
public override async Task<FileInfo?> Download(IProgress<double> progress)
|
||||||
{
|
{
|
||||||
var megaClient = new MegaApiClient();
|
var megaClient = new MegaApiClient();
|
||||||
await megaClient.LoginAnonymousAsync();
|
await megaClient.LoginAnonymousAsync();
|
||||||
|
|
||||||
// if mega fails to connect, just return
|
// if mega fails to connect, just return
|
||||||
if (!megaClient.IsLoggedIn)
|
if (!megaClient.IsLoggedIn)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var file = new FileInfo(Path.Join(DownloadCacheHelper.CachePath, "patcher"));
|
var file = new FileInfo(Path.Join(DownloadCacheHelper.CachePath, "patcher"));
|
||||||
|
|
||||||
|
if (file.Exists)
|
||||||
|
{
|
||||||
|
file.Delete();
|
||||||
|
}
|
||||||
|
|
||||||
await megaClient.DownloadFileAsync(new Uri(MirrorInfo.Link),
|
await megaClient.DownloadFileAsync(new Uri(MirrorInfo.Link),
|
||||||
file.FullName, progress);
|
file.FullName, progress);
|
||||||
|
|
||||||
file.Refresh();
|
file.Refresh();
|
||||||
|
|
||||||
if (!file.Exists)
|
if (!file.Exists)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return FileHashHelper.CheckHash(file, MirrorInfo.Hash) ? file : null;
|
return FileHashHelper.CheckHash(file, MirrorInfo.Hash) ? file : null;
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Exception thrown while downloading from mega");
|
Log.Error(ex, "Exception thrown while downloading from mega");
|
||||||
//most likely a 509 (Bandwidth limit exceeded) due to mega's user quotas.
|
//most likely a 509 (Bandwidth limit exceeded) due to mega's user quotas.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,12 +2,14 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SPTInstaller.Models.Mirrors.Downloaders;
|
namespace SPTInstaller.Models.Mirrors.Downloaders;
|
||||||
|
|
||||||
public abstract class MirrorDownloaderBase : IMirrorDownloader
|
public abstract class MirrorDownloaderBase : IMirrorDownloader
|
||||||
{
|
{
|
||||||
public DownloadMirror MirrorInfo { get; private set; }
|
public PatchInfoMirror MirrorInfo { get; private set; }
|
||||||
public abstract Task<FileInfo?> Download(IProgress<double> progress);
|
public abstract Task<FileInfo?> Download(IProgress<double> progress);
|
||||||
public MirrorDownloaderBase(DownloadMirror mirrorInfo)
|
|
||||||
|
public MirrorDownloaderBase(PatchInfoMirror mirrorInfo)
|
||||||
{
|
{
|
||||||
MirrorInfo = mirrorInfo;
|
MirrorInfo = mirrorInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
10
SPTInstaller/Models/Mirrors/PatchInfo.cs
Normal file
10
SPTInstaller/Models/Mirrors/PatchInfo.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace SPTInstaller.Models.Mirrors;
|
||||||
|
|
||||||
|
public class PatchInfo
|
||||||
|
{
|
||||||
|
public int SourceClientVersion { get; set; }
|
||||||
|
public int TargetClientVersion { get; set; }
|
||||||
|
public List<PatchInfoMirror> Mirrors { get; set; }
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
namespace SPTInstaller.Models.Mirrors;
|
namespace SPTInstaller.Models.Mirrors;
|
||||||
|
|
||||||
public class DownloadMirror
|
public class PatchInfoMirror
|
||||||
{
|
{
|
||||||
public string Link { get; set; }
|
public string Link { get; set; }
|
||||||
public string Hash { get; set; }
|
public string Hash { get; set; }
|
32
SPTInstaller/Models/PathCheck.cs
Normal file
32
SPTInstaller/Models/PathCheck.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
namespace SPTInstaller.Models;
|
||||||
|
|
||||||
|
public enum PathCheckType
|
||||||
|
{
|
||||||
|
EndsWith = 0,
|
||||||
|
Contains = 1,
|
||||||
|
DriveRoot = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PathCheckAction
|
||||||
|
{
|
||||||
|
Warn = 0,
|
||||||
|
Deny = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PathCheck
|
||||||
|
{
|
||||||
|
public string Target { get; private set; }
|
||||||
|
public PathCheckType CheckType { get; private set; }
|
||||||
|
public PathCheckAction CheckAction { get; private set; }
|
||||||
|
|
||||||
|
public PathCheck()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PathCheck(string target, PathCheckType checkType, PathCheckAction checkAction)
|
||||||
|
{
|
||||||
|
Target = target;
|
||||||
|
CheckType = checkType;
|
||||||
|
CheckAction = checkAction;
|
||||||
|
}
|
||||||
|
}
|
@ -8,63 +8,79 @@ namespace SPTInstaller.Models;
|
|||||||
|
|
||||||
public abstract class PreCheckBase : ReactiveObject, IPreCheck
|
public abstract class PreCheckBase : ReactiveObject, IPreCheck
|
||||||
{
|
{
|
||||||
public event EventHandler ReeevaluationRequested = delegate { };
|
public event EventHandler ReeevaluationRequested = delegate { };
|
||||||
private string _id;
|
private string _id;
|
||||||
|
|
||||||
public string Id
|
public string Id
|
||||||
{
|
{
|
||||||
get => _id;
|
get => _id;
|
||||||
set => this.RaiseAndSetIfChanged(ref _id, value);
|
set => this.RaiseAndSetIfChanged(ref _id, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _isSelected;
|
||||||
|
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => _isSelected;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
|
||||||
|
}
|
||||||
|
|
||||||
private string _name;
|
private string _name;
|
||||||
|
|
||||||
public string Name
|
public string Name
|
||||||
{
|
{
|
||||||
get => _name;
|
get => _name;
|
||||||
set => this.RaiseAndSetIfChanged(ref _name, value);
|
set => this.RaiseAndSetIfChanged(ref _name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _required;
|
private bool _required;
|
||||||
|
|
||||||
public bool IsRequired
|
public bool IsRequired
|
||||||
{
|
{
|
||||||
get => _required;
|
get => _required;
|
||||||
set => this.RaiseAndSetIfChanged(ref _required, value);
|
set => this.RaiseAndSetIfChanged(ref _required, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private StatusSpinner.SpinnerState _state;
|
private StatusSpinner.SpinnerState _state;
|
||||||
|
|
||||||
public StatusSpinner.SpinnerState State
|
public StatusSpinner.SpinnerState State
|
||||||
{
|
{
|
||||||
get => _state;
|
get => _state;
|
||||||
set => this.RaiseAndSetIfChanged(ref _state, value);
|
set => this.RaiseAndSetIfChanged(ref _state, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _preCheckDetails;
|
private string _preCheckDetails;
|
||||||
|
|
||||||
public string PreCheckDetails
|
public string PreCheckDetails
|
||||||
{
|
{
|
||||||
get => _preCheckDetails;
|
get => _preCheckDetails;
|
||||||
set => this.RaiseAndSetIfChanged(ref _preCheckDetails, value);
|
set => this.RaiseAndSetIfChanged(ref _preCheckDetails, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _actionButtonIsVisible;
|
private bool _actionButtonIsVisible;
|
||||||
|
|
||||||
public bool ActionButtonIsVisible
|
public bool ActionButtonIsVisible
|
||||||
{
|
{
|
||||||
get => _actionButtonIsVisible;
|
get => _actionButtonIsVisible;
|
||||||
set => this.RaiseAndSetIfChanged(ref _actionButtonIsVisible, value);
|
set => this.RaiseAndSetIfChanged(ref _actionButtonIsVisible, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _actionButtonText;
|
private string _actionButtonText;
|
||||||
|
|
||||||
public string ActionButtonText
|
public string ActionButtonText
|
||||||
{
|
{
|
||||||
get => _actionButtonText;
|
get => _actionButtonText;
|
||||||
set => this.RaiseAndSetIfChanged(ref _actionButtonText, value);
|
set => this.RaiseAndSetIfChanged(ref _actionButtonText, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ICommand _actionButtonCommand;
|
private ICommand _actionButtonCommand;
|
||||||
|
|
||||||
public ICommand ActionButtonCommand
|
public ICommand ActionButtonCommand
|
||||||
{
|
{
|
||||||
get => _actionButtonCommand;
|
get => _actionButtonCommand;
|
||||||
set => this.RaiseAndSetIfChanged(ref _actionButtonCommand, value);
|
set => this.RaiseAndSetIfChanged(ref _actionButtonCommand, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for pre-checks to run before installation
|
/// Base class for pre-checks to run before installation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -76,15 +92,15 @@ public abstract class PreCheckBase : ReactiveObject, IPreCheck
|
|||||||
IsRequired = required;
|
IsRequired = required;
|
||||||
Id = Guid.NewGuid().ToString();
|
Id = Guid.NewGuid().ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private StatusSpinner.SpinnerState ProcessResult(PreCheckResult result) =>
|
private StatusSpinner.SpinnerState ProcessResult(PreCheckResult result) =>
|
||||||
(result.Succeeded, IsRequired) switch
|
(result.Succeeded, IsRequired) switch
|
||||||
{
|
{
|
||||||
(true, _) => StatusSpinner.SpinnerState.OK,
|
(true, _) => StatusSpinner.SpinnerState.OK,
|
||||||
(false, false) => StatusSpinner.SpinnerState.Warning,
|
(false, false) => StatusSpinner.SpinnerState.Warning,
|
||||||
(_, _) => StatusSpinner.SpinnerState.Error
|
(_, _) => StatusSpinner.SpinnerState.Error
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request pre-checks to be re-evaluated
|
/// Request pre-checks to be re-evaluated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -92,25 +108,29 @@ public abstract class PreCheckBase : ReactiveObject, IPreCheck
|
|||||||
{
|
{
|
||||||
ReeevaluationRequested?.Invoke(this, null);
|
ReeevaluationRequested?.Invoke(this, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IResult> RunCheck()
|
public async Task<IResult> RunCheck()
|
||||||
{
|
{
|
||||||
State = StatusSpinner.SpinnerState.Running;
|
State = StatusSpinner.SpinnerState.Running;
|
||||||
|
|
||||||
var result = await CheckOperation();
|
var result = await CheckOperation();
|
||||||
|
|
||||||
PreCheckDetails = !string.IsNullOrWhiteSpace(result.Message)
|
PreCheckDetails = !string.IsNullOrWhiteSpace(result.Message)
|
||||||
? result.Message
|
? result.Message
|
||||||
: (result.Succeeded ? "Pre-Check succeeded, but no details were provided" : "Pre-Check failed, but no details were provided");
|
: (result.Succeeded
|
||||||
|
? "Pre-Check succeeded, but no details were provided"
|
||||||
|
: "Pre-Check failed, but no details were provided");
|
||||||
|
|
||||||
ActionButtonText = result.ActionButtonText;
|
ActionButtonText = result.ActionButtonText;
|
||||||
ActionButtonCommand = result.ButtonPressedCommand;
|
ActionButtonCommand = result.ButtonPressedCommand;
|
||||||
ActionButtonIsVisible = result.ActionButtonIsVisible;
|
ActionButtonIsVisible = result.ActionButtonIsVisible;
|
||||||
|
|
||||||
State = ProcessResult(result);
|
State = ProcessResult(result);
|
||||||
|
|
||||||
return State == StatusSpinner.SpinnerState.OK ? Result.FromSuccess() : Result.FromError($"PreCheck Failed: {Name}");
|
return State == StatusSpinner.SpinnerState.OK
|
||||||
|
? Result.FromSuccess()
|
||||||
|
: Result.FromError($"PreCheck Failed: {Name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Task<PreCheckResult> CheckOperation();
|
public abstract Task<PreCheckResult> CheckOperation();
|
||||||
}
|
}
|
64
SPTInstaller/Models/PreCheckDetailInfo.cs
Normal file
64
SPTInstaller/Models/PreCheckDetailInfo.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using ReactiveUI;
|
||||||
|
using SPTInstaller.CustomControls;
|
||||||
|
|
||||||
|
namespace SPTInstaller.Models;
|
||||||
|
|
||||||
|
public class PreCheckDetailInfo : ReactiveObject
|
||||||
|
{
|
||||||
|
public PreCheckDetailInfo()
|
||||||
|
{
|
||||||
|
_name = "";
|
||||||
|
_details = "";
|
||||||
|
_actionButtonText = "";
|
||||||
|
_showActionButton = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _name;
|
||||||
|
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _details;
|
||||||
|
|
||||||
|
public string Details
|
||||||
|
{
|
||||||
|
get => _details;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _details, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _actionButtonText;
|
||||||
|
|
||||||
|
public string ActionButtonText
|
||||||
|
{
|
||||||
|
get => _actionButtonText;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _actionButtonText, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ICommand _actionButtonCommand;
|
||||||
|
|
||||||
|
public ICommand ActionButtonCommand
|
||||||
|
{
|
||||||
|
get => _actionButtonCommand;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _actionButtonCommand, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _showActionButton;
|
||||||
|
|
||||||
|
public bool ShowActionButton
|
||||||
|
{
|
||||||
|
get => _showActionButton;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _showActionButton, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _barColor;
|
||||||
|
|
||||||
|
public string BarColor
|
||||||
|
{
|
||||||
|
get => _barColor;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _barColor, value);
|
||||||
|
}
|
||||||
|
}
|
@ -3,35 +3,42 @@ using SPTInstaller.Interfaces;
|
|||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace SPTInstaller.Models;
|
namespace SPTInstaller.Models;
|
||||||
|
|
||||||
public class PreCheckResult : IResult
|
public class PreCheckResult : IResult
|
||||||
{
|
{
|
||||||
public bool Succeeded { get; private set; }
|
public bool Succeeded { get; private set; }
|
||||||
|
|
||||||
public string Message { get; private set; }
|
public string Message { get; private set; }
|
||||||
|
|
||||||
public bool ActionButtonIsVisible { get; private set; }
|
public bool ActionButtonIsVisible { get; private set; }
|
||||||
|
|
||||||
public string ActionButtonText { get; private set; }
|
public string ActionButtonText { get; private set; }
|
||||||
|
|
||||||
public ICommand ButtonPressedCommand { get; private set; }
|
public ICommand ButtonPressedCommand { get; private set; }
|
||||||
|
|
||||||
protected PreCheckResult(string message, bool succeeded, string actionButtonText, Action? buttonPressedAction)
|
protected PreCheckResult(string message, bool succeeded, string actionButtonText, Action? buttonPressedAction)
|
||||||
{
|
{
|
||||||
Message = message;
|
Message = message;
|
||||||
Succeeded = succeeded;
|
Succeeded = succeeded;
|
||||||
|
|
||||||
ActionButtonText = actionButtonText;
|
ActionButtonText = actionButtonText;
|
||||||
|
|
||||||
ActionButtonIsVisible = buttonPressedAction != null && !string.IsNullOrWhiteSpace(actionButtonText);
|
ActionButtonIsVisible = buttonPressedAction != null && !string.IsNullOrWhiteSpace(actionButtonText);
|
||||||
|
|
||||||
buttonPressedAction ??= () => { };
|
buttonPressedAction ??= () => { };
|
||||||
|
|
||||||
ButtonPressedCommand = ReactiveCommand.Create(buttonPressedAction);
|
ButtonPressedCommand = ReactiveCommand.Create(buttonPressedAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PreCheckResult FromSuccess(string message = "") => new PreCheckResult(message, true, "", null);
|
public static PreCheckResult FromSuccess(string message = "") => new PreCheckResult(message, true, "", null);
|
||||||
|
|
||||||
public static PreCheckResult FromError(string message, string actionButtonText = "", Action? actionButtonPressedAction = null) => new PreCheckResult(message, false, actionButtonText, actionButtonPressedAction);
|
public static PreCheckResult FromError(string message, string actionButtonText = "",
|
||||||
|
Action? actionButtonPressedAction = null) =>
|
||||||
public static PreCheckResult FromException(Exception ex, string actionButtonText = "", Action? actionButtonPressedAction = null) => new PreCheckResult($"An exception was thrown during this precheck\n\nException:\n{ex.Message}\n\nStacktrace:\n{ex.StackTrace}", false, actionButtonText, actionButtonPressedAction);
|
new PreCheckResult(message, false, actionButtonText, actionButtonPressedAction);
|
||||||
}
|
|
||||||
|
public static PreCheckResult
|
||||||
|
FromException(Exception ex, string actionButtonText = "", Action? actionButtonPressedAction = null) =>
|
||||||
|
new PreCheckResult(
|
||||||
|
$"An exception was thrown during this precheck\n\nException:\n{ex.Message}\n\nStacktrace:\n{ex.StackTrace}",
|
||||||
|
false, actionButtonText, actionButtonPressedAction);
|
||||||
|
}
|
@ -5,14 +5,15 @@ public class ReadProcessResult : Result
|
|||||||
public string StdOut { get; }
|
public string StdOut { get; }
|
||||||
public string StdErr { get; }
|
public string StdErr { get; }
|
||||||
|
|
||||||
protected ReadProcessResult(string message, bool succeeded, string stdOut = "", string stdErr = "") : base(message, succeeded)
|
protected ReadProcessResult(string message, bool succeeded, string stdOut = "", string stdErr = "") : base(message,
|
||||||
|
succeeded)
|
||||||
{
|
{
|
||||||
StdOut = stdOut;
|
StdOut = stdOut;
|
||||||
StdErr = stdErr;
|
StdErr = stdErr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ReadProcessResult FromSuccess(string stdOut, string stdErr) =>
|
public static ReadProcessResult FromSuccess(string stdOut, string stdErr) =>
|
||||||
new ReadProcessResult("ok", true, stdOut, stdErr);
|
new ReadProcessResult("ok", true, stdOut, stdErr);
|
||||||
|
|
||||||
public new static ReadProcessResult FromError(string message) => new ReadProcessResult(message, false);
|
public new static ReadProcessResult FromError(string message) => new ReadProcessResult(message, false);
|
||||||
}
|
}
|
12
SPTInstaller/Models/ReleaseInfo/ReleaseInfo.cs
Normal file
12
SPTInstaller/Models/ReleaseInfo/ReleaseInfo.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace SPTInstaller.Models.ReleaseInfo;
|
||||||
|
|
||||||
|
public class ReleaseInfo
|
||||||
|
{
|
||||||
|
[JsonProperty("AkiVersion")] // TODO: Change this and what gets uploaded to SPTVersion
|
||||||
|
public string SPTVersion { get; set; }
|
||||||
|
public string ClientVersion { get; set; }
|
||||||
|
public List<ReleaseInfoMirror> Mirrors { get; set; }
|
||||||
|
}
|
7
SPTInstaller/Models/ReleaseInfo/ReleaseInfoMirror.cs
Normal file
7
SPTInstaller/Models/ReleaseInfo/ReleaseInfoMirror.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace SPTInstaller.Models.ReleaseInfo;
|
||||||
|
|
||||||
|
public class ReleaseInfoMirror
|
||||||
|
{
|
||||||
|
public string DownloadUrl { get; set; }
|
||||||
|
public string Hash { get; set; }
|
||||||
|
}
|
@ -5,15 +5,15 @@ namespace SPTInstaller.Models;
|
|||||||
public class Result : IResult
|
public class Result : IResult
|
||||||
{
|
{
|
||||||
public bool Succeeded { get; private set; }
|
public bool Succeeded { get; private set; }
|
||||||
|
|
||||||
public string Message { get; private set; }
|
public string Message { get; private set; }
|
||||||
|
|
||||||
protected Result(string message, bool succeeded)
|
protected Result(string message, bool succeeded)
|
||||||
{
|
{
|
||||||
Message = message;
|
Message = message;
|
||||||
Succeeded = succeeded;
|
Succeeded = succeeded;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Result FromSuccess(string message = "") => new(message, true);
|
public static Result FromSuccess(string message = "") => new(message, true);
|
||||||
public static Result FromError(string message) => new(message, false);
|
public static Result FromError(string message) => new(message, false);
|
||||||
}
|
}
|
@ -11,6 +11,7 @@ using SPTInstaller.Models;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using SPTInstaller.CustomControls;
|
||||||
|
|
||||||
namespace SPTInstaller;
|
namespace SPTInstaller;
|
||||||
|
|
||||||
@ -32,23 +33,23 @@ internal class Program
|
|||||||
Log.Fatal(ex, "Installer closed unexpectedly");
|
Log.Fatal(ex, "Installer closed unexpectedly");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avalonia configuration, don't remove; also used by visual designer.
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
{
|
{
|
||||||
Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly());
|
Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
// Register all the things
|
// Register all the things
|
||||||
// Regestering as base classes so ReactiveUI works correctly. Doesn't seem to like the interfaces :(
|
// Regestering as base classes so ReactiveUI works correctly. Doesn't seem to like the interfaces :(
|
||||||
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, NetCore6PreCheck>();
|
|
||||||
ServiceHelper.Register<PreCheckBase, Net8PreCheck>();
|
ServiceHelper.Register<PreCheckBase, Net8PreCheck>();
|
||||||
ServiceHelper.Register<PreCheckBase, FreeSpacePreCheck>();
|
ServiceHelper.Register<PreCheckBase, FreeSpacePreCheck>();
|
||||||
ServiceHelper.Register<PreCheckBase, EftLauncherPreCheck>();
|
ServiceHelper.Register<PreCheckBase, EftLauncherPreCheck>();
|
||||||
|
|
||||||
ServiceHelper.Register<InstallerTaskBase, InitializationTask>();
|
ServiceHelper.Register<InstallerTaskBase, InitializationTask>();
|
||||||
ServiceHelper.Register<InstallerTaskBase, ReleaseCheckTask>();
|
ServiceHelper.Register<InstallerTaskBase, ReleaseCheckTask>();
|
||||||
ServiceHelper.Register<InstallerTaskBase, DownloadTask>();
|
ServiceHelper.Register<InstallerTaskBase, DownloadTask>();
|
||||||
@ -64,16 +65,16 @@ internal class Program
|
|||||||
Locator.CurrentMutable.RegisterConstant<PreCheckBase>(TestPreCheck.FromRandomName(StatusSpinner.SpinnerState.Warning));
|
Locator.CurrentMutable.RegisterConstant<PreCheckBase>(TestPreCheck.FromRandomName(StatusSpinner.SpinnerState.Warning));
|
||||||
Locator.CurrentMutable.RegisterConstant<PreCheckBase>(TestPreCheck.FromRandomName(StatusSpinner.SpinnerState.Error));
|
Locator.CurrentMutable.RegisterConstant<PreCheckBase>(TestPreCheck.FromRandomName(StatusSpinner.SpinnerState.Error));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// need the interfaces for the controller and splat won't resolve them since we need to base classes in avalonia (what a mess), so doing it manually here
|
// need the interfaces for the controller and splat won't resolve them since we need to base classes in avalonia (what a mess), so doing it manually here
|
||||||
var tasks = Locator.Current.GetServices<InstallerTaskBase>().ToArray() as IProgressableTask[];
|
var tasks = Locator.Current.GetServices<InstallerTaskBase>().ToArray() as IProgressableTask[];
|
||||||
var preChecks = Locator.Current.GetServices<PreCheckBase>().ToArray() as IPreCheck[];
|
var preChecks = Locator.Current.GetServices<PreCheckBase>().ToArray() as IPreCheck[];
|
||||||
|
|
||||||
var installer = new InstallController(tasks, preChecks);
|
var installer = new InstallController(tasks, preChecks);
|
||||||
|
|
||||||
// manually register install controller
|
// manually register install controller
|
||||||
Locator.CurrentMutable.RegisterConstant(installer);
|
Locator.CurrentMutable.RegisterConstant(installer);
|
||||||
|
|
||||||
return AppBuilder.Configure<App>()
|
return AppBuilder.Configure<App>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
.LogToTrace()
|
.LogToTrace()
|
||||||
|
Binary file not shown.
23
SPTInstaller/Resources/add_shortcuts.ps1
Normal file
23
SPTInstaller/Resources/add_shortcuts.ps1
Normal 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()
|
@ -5,22 +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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<linker>
|
<linker>
|
||||||
<!-- Can be removed if CompiledBinding and no reflection are used -->
|
<!-- Can be removed if CompiledBinding and no reflection are used -->
|
||||||
<assembly fullname="SPTInstaller" preserve="All" />
|
<assembly fullname="SPTInstaller" preserve="All"/>
|
||||||
<assembly fullname="Avalonia.Themes.Fluent" preserve="All" />
|
<assembly fullname="Avalonia.Themes.Fluent" preserve="All"/>
|
||||||
</linker>
|
</linker>
|
||||||
|
@ -1,56 +1,53 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<PackageIcon>icon.ico</PackageIcon>
|
<!-- TODO: To change -->
|
||||||
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
|
<PackageIcon>icon.ico</PackageIcon>
|
||||||
<Configurations>Debug;Release;TEST</Configurations>
|
<ApplicationIcon>Assets\spt_installer.ico</ApplicationIcon>
|
||||||
<AssemblyVersion>2.44</AssemblyVersion>
|
<Configurations>Debug;Release;TEST</Configurations>
|
||||||
<FileVersion>2.44</FileVersion>
|
<AssemblyVersion>2.90</AssemblyVersion>
|
||||||
<Company>SPT-AKI</Company>
|
<FileVersion>2.90</FileVersion>
|
||||||
</PropertyGroup>
|
<Company>SPT</Company>
|
||||||
|
</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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<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"/>
|
||||||
</ItemGroup>
|
<None Remove="Resources\add_shortcuts.ps1" />
|
||||||
|
<EmbeddedResource Include="Resources\add_shortcuts.ps1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<TrimmerRootDescriptor Include="Roots.xml" />
|
<TrimmerRootDescriptor Include="Roots.xml"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Include="Avalonia" Version="11.0.5" />
|
<PackageReference Include="Avalonia" Version="11.0.5"/>
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.0.5"/>
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.5" />
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.5"/>
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.5" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.5"/>
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5"/>
|
||||||
<PackageReference Include="DialogHost.Avalonia" Version="0.7.7" />
|
<PackageReference Include="DialogHost.Avalonia" Version="0.7.7"/>
|
||||||
<PackageReference Include="FubarCoder.RestSharp.Portable.HttpClient" Version="4.0.8" />
|
<PackageReference Include="FubarCoder.RestSharp.Portable.HttpClient" Version="4.0.8"/>
|
||||||
<PackageReference Include="MegaApiClient" Version="1.10.4" />
|
<PackageReference Include="MegaApiClient" Version="1.10.4"/>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/>
|
||||||
<PackageReference Include="SerilogTraceListener" Version="3.2.0" />
|
<PackageReference Include="SerilogTraceListener" Version="3.2.0"/>
|
||||||
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.1.23" />
|
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.1.23"/>
|
||||||
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
<PackageReference Include="System.Reactive" Version="6.0.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="Gitea">
|
|
||||||
<HintPath>Z:\dev_stuff\EftPatchHelper\EftPatchHelper\EftPatchHelper\Resources\Gitea.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -10,15 +10,15 @@ public class ViewLocator : IDataTemplate
|
|||||||
{
|
{
|
||||||
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
||||||
var type = Type.GetType(name);
|
var type = Type.GetType(name);
|
||||||
|
|
||||||
if (type != null)
|
if (type != null)
|
||||||
{
|
{
|
||||||
return (Control)Activator.CreateInstance(type)!;
|
return (Control)Activator.CreateInstance(type)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new TextBlock { Text = "Not Found: " + name };
|
return new TextBlock { Text = "Not Found: " + name };
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Match(object data)
|
public bool Match(object data)
|
||||||
{
|
{
|
||||||
return data is ViewModelBase;
|
return data is ViewModelBase;
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace SPTInstaller.ViewModels;
|
|
||||||
public class DetailedPreChecksViewModel : PreChecksViewModel
|
|
||||||
{
|
|
||||||
public DetailedPreChecksViewModel(IScreen host, bool debugging) : base(host, debugging)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
157
SPTInstaller/ViewModels/InstallPathSelectionViewModel.cs
Normal file
157
SPTInstaller/ViewModels/InstallPathSelectionViewModel.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
@ -11,24 +11,26 @@ namespace SPTInstaller.ViewModels;
|
|||||||
public class InstallViewModel : ViewModelBase
|
public class InstallViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private IProgressableTask _currentTask;
|
private IProgressableTask _currentTask;
|
||||||
|
|
||||||
public IProgressableTask CurrentTask
|
public IProgressableTask CurrentTask
|
||||||
{
|
{
|
||||||
get => _currentTask;
|
get => _currentTask;
|
||||||
set => this.RaiseAndSetIfChanged(ref _currentTask, value);
|
set => this.RaiseAndSetIfChanged(ref _currentTask, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<InstallerTaskBase> MyTasks { get; set; } = new(ServiceHelper.GetAll<InstallerTaskBase>());
|
public ObservableCollection<InstallerTaskBase> MyTasks { get; set; } =
|
||||||
|
new(ServiceHelper.GetAll<InstallerTaskBase>());
|
||||||
|
|
||||||
public InstallViewModel(IScreen host) : base(host)
|
public InstallViewModel(IScreen host) : base(host)
|
||||||
{
|
{
|
||||||
var installer = ServiceHelper.Get<InstallController>();
|
var installer = ServiceHelper.Get<InstallController>();
|
||||||
|
|
||||||
installer.TaskChanged += (sender, task) => CurrentTask = task;
|
installer.TaskChanged += (sender, task) => CurrentTask = task;
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var result = await installer.RunTasks();
|
var result = await installer.RunTasks();
|
||||||
|
|
||||||
NavigateTo(new MessageViewModel(HostScreen, result));
|
NavigateTo(new MessageViewModel(HostScreen, result));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
42
SPTInstaller/ViewModels/InstallerUpdateViewModel.cs
Normal file
42
SPTInstaller/ViewModels/InstallerUpdateViewModel.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Gitea.Client;
|
|
||||||
using ReactiveUI;
|
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;
|
||||||
|
|
||||||
@ -11,41 +12,45 @@ public class MainWindowViewModel : ReactiveObject, IActivatableViewModel, IScree
|
|||||||
{
|
{
|
||||||
public RoutingState Router { get; } = new();
|
public RoutingState Router { get; } = new();
|
||||||
public ViewModelActivator Activator { get; } = new();
|
public ViewModelActivator Activator { get; } = new();
|
||||||
|
|
||||||
private string _title;
|
private string _title;
|
||||||
|
|
||||||
public string Title
|
public string Title
|
||||||
{
|
{
|
||||||
get => _title;
|
get => _title;
|
||||||
set => this.RaiseAndSetIfChanged(ref _title, value);
|
set => this.RaiseAndSetIfChanged(ref _title, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MainWindowViewModel(bool debugging)
|
public MainWindowViewModel(string installPath)
|
||||||
{
|
{
|
||||||
Configuration.Default.BasePath = "https://dev.sp-tarkov.com/api/v1";
|
var data = ServiceHelper.Get<InternalData>() ?? throw new Exception("failed to get interanl data");
|
||||||
|
|
||||||
Title = $"{(debugging ? "-debug-" : "")} SPT Installer {"v" + Assembly.GetExecutingAssembly().GetName()?.Version?.ToString() ?? "--unknown version--"}";
|
Title =
|
||||||
|
$"{(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);
|
||||||
|
|
||||||
var uiCulture= CultureInfo.InstalledUICulture;
|
var uiCulture = CultureInfo.InstalledUICulture;
|
||||||
|
|
||||||
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()
|
||||||
{
|
{
|
||||||
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
|
if (Application.Current.ApplicationLifetime is
|
||||||
|
Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
|
||||||
{
|
{
|
||||||
desktopApp.MainWindow.Close();
|
desktopApp.MainWindow.Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MinimizeCommand()
|
public void MinimizeCommand()
|
||||||
{
|
{
|
||||||
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
|
if (Application.Current.ApplicationLifetime is
|
||||||
|
Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
|
||||||
{
|
{
|
||||||
desktopApp.MainWindow.WindowState = Avalonia.Controls.WindowState.Minimized;
|
desktopApp.MainWindow.WindowState = Avalonia.Controls.WindowState.Minimized;
|
||||||
}
|
}
|
||||||
|
@ -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,76 +8,231 @@ 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;
|
||||||
|
|
||||||
public class MessageViewModel : ViewModelBase
|
public class MessageViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private bool _HasErrors;
|
private bool _HasErrors;
|
||||||
|
|
||||||
public bool HasErrors
|
public bool HasErrors
|
||||||
{
|
{
|
||||||
get => _HasErrors;
|
get => _HasErrors;
|
||||||
set => this.RaiseAndSetIfChanged(ref _HasErrors, value);
|
set => this.RaiseAndSetIfChanged(ref _HasErrors, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _Message;
|
private string _Message;
|
||||||
|
|
||||||
public string Message
|
public string Message
|
||||||
{
|
{
|
||||||
get => _Message;
|
get => _Message;
|
||||||
set => this.RaiseAndSetIfChanged(ref _Message, value);
|
set => this.RaiseAndSetIfChanged(ref _Message, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _showCloseButton;
|
private bool _showCloseButton;
|
||||||
|
|
||||||
public bool ShowCloseButton
|
public bool ShowCloseButton
|
||||||
{
|
{
|
||||||
get => _showCloseButton;
|
get => _showCloseButton;
|
||||||
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
|
||||||
{
|
{
|
||||||
get => _cacheInfoText;
|
get => _cacheInfoText;
|
||||||
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
|
||||||
{
|
{
|
||||||
get => _cacheCheckState;
|
get => _cacheCheckState;
|
||||||
set => this.RaiseAndSetIfChanged(ref _cacheCheckState, value);
|
set => this.RaiseAndSetIfChanged(ref _cacheCheckState, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICommand CloseCommand { get; set; } = ReactiveCommand.Create(() =>
|
public ICommand CloseCommand { get; set; } = ReactiveCommand.Create(() =>
|
||||||
{
|
{
|
||||||
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
|
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopApp)
|
||||||
{
|
{
|
||||||
desktopApp.MainWindow.Close();
|
desktopApp.MainWindow.Close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
public MessageViewModel(IScreen Host, IResult result, bool showCloseButton = true, bool noLog = false) : base(Host)
|
public MessageViewModel(IScreen Host, IResult result, bool showCloseButton = true, bool noLog = false) : base(Host)
|
||||||
{
|
{
|
||||||
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(() =>
|
||||||
{
|
{
|
||||||
CacheInfoText = "Getting cache size ...";
|
CacheInfoText = "Getting cache size ...";
|
||||||
CacheCheckState = StatusSpinner.SpinnerState.Running;
|
CacheCheckState = StatusSpinner.SpinnerState.Running;
|
||||||
|
|
||||||
CacheInfoText = $"Cache Size: {DownloadCacheHelper.GetCacheSizeText()}";
|
CacheInfoText = $"Cache Size: {DownloadCacheHelper.GetCacheSizeText()}";
|
||||||
CacheCheckState = StatusSpinner.SpinnerState.OK;
|
CacheCheckState = StatusSpinner.SpinnerState.OK;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
Log.Information(Message);
|
Log.Information(Message);
|
||||||
|
ShowOptions = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HasErrors = true;
|
HasErrors = true;
|
||||||
|
|
||||||
if (!noLog)
|
if (!noLog)
|
||||||
Log.Error(Message);
|
Log.Error(Message);
|
||||||
}
|
}
|
||||||
|
23
SPTInstaller/ViewModels/OverviewViewModel.cs
Normal file
23
SPTInstaller/ViewModels/OverviewViewModel.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user