Compare commits

...

10 Commits

95 changed files with 1868 additions and 1730 deletions

View File

@ -1,5 +1,4 @@

[*]
[*]
charset = utf-8-bom
end_of_line = crlf
trim_trailing_whitespace = false
@ -8,7 +7,7 @@ indent_style = space
indent_size = 4
# 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_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

View File

@ -7,6 +7,9 @@
<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/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" />

View File

@ -5,7 +5,7 @@
xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
RequestedThemeVariant="Light">
<Application.DataTemplates>
<local:ViewLocator/>
<local:ViewLocator />
</Application.DataTemplates>
<Application.Styles>
@ -13,38 +13,40 @@
<dialogHostAvalonia:DialogHostStyles />
</Application.Styles>
<Application.Resources>
<Application.Resources>
<!-- Colors -->
<Color x:Key="AKI_DarkGray">#121212</Color>
<Color x:Key="AKI_Yellow">#FFC107</Color>
<Color x:Key="AKI_White">#FFFFFF</Color>
<Color x:Key="AKI_Gray">#282828</Color>
<Color x:Key="AKI_DarkGrayBlue">#323947</Color>
<!-- Colors -->
<Color x:Key="AKI_DarkGray">#121212</Color>
<Color x:Key="AKI_Yellow">#FFC107</Color>
<Color x:Key="AKI_White">#FFFFFF</Color>
<Color x:Key="AKI_Gray">#282828</Color>
<Color x:Key="AKI_DarkGrayBlue">#323947</Color>
<Color x:Key="AKI_LightGrayBlue">#444259</Color>
<!-- Brushes -->
<SolidColorBrush x:Key="AKI_Foreground_Light" Color="{StaticResource AKI_White}"/>
<SolidColorBrush x:Key="AKI_Background_Light" Color="{StaticResource AKI_Gray}"/>
<SolidColorBrush x:Key="AKI_Background_Dark" Color="{StaticResource AKI_DarkGray}"/>
<SolidColorBrush x:Key="AKI_Brush_Yellow" Color="{StaticResource AKI_Yellow}"/>
<SolidColorBrush x:Key="AKI_Brush_DarkGrayBlue" Color="{StaticResource AKI_DarkGrayBlue}"/>
<SolidColorBrush x:Key="AKI_Brush_LightGrayBlue" Color="{StaticResource AKI_LightGrayBlue}"/>
<SolidColorBrush x:Key="AKI_Brush_Lighter" Color="Gainsboro"/>
<!-- Brushes -->
<SolidColorBrush x:Key="AKI_Foreground_Light" Color="{StaticResource AKI_White}" />
<SolidColorBrush x:Key="AKI_Background_Light" Color="{StaticResource AKI_Gray}" />
<SolidColorBrush x:Key="AKI_Background_Dark" Color="{StaticResource AKI_DarkGray}" />
<SolidColorBrush x:Key="AKI_Brush_Yellow" Color="{StaticResource AKI_Yellow}" />
<SolidColorBrush x:Key="AKI_Brush_DarkGrayBlue" Color="{StaticResource AKI_DarkGrayBlue}" />
<SolidColorBrush x:Key="AKI_Brush_LightGrayBlue" Color="{StaticResource AKI_LightGrayBlue}" />
<SolidColorBrush x:Key="AKI_Brush_Lighter" Color="Gainsboro" />
<!-- 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"
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="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"
/>
<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"
/>
<!-- 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"
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="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" />
<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"
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"
/>
</Application.Resources>
</Application>
FillRule="NonZero" />
</Application.Resources>
</Application>

View File

@ -13,6 +13,7 @@ namespace SPTInstaller;
public partial class App : Application
{
private readonly string _logPath = Path.Join(Environment.CurrentDirectory, "spt-aki-installer_.log");
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@ -24,14 +25,13 @@ public partial class App : Application
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information,
rollingInterval: RollingInterval.Day)
.CreateLogger();
RxApp.DefaultExceptionHandler = Observer.Create<Exception>((exception) =>
{
Log.Error(exception, "An application exception occurred");
});
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
@ -57,7 +57,7 @@ public partial class App : Application
DataContext = new MainWindowViewModel(debug),
};
}
base.OnFrameworkInitializationCompleted();
}
}

View File

@ -1,267 +1,268 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="using:SPTInstaller.CustomControls"
>
<Design.PreviewWith>
<StackPanel Spacing="5" Background="{StaticResource AKI_Background_Dark}">
<Button Classes="icon" x:Name="testBtn">
<Path Data="{StaticResource Bug}"
Fill="{Binding ElementName=testBtn, Path=Foreground}"
/>
xmlns:cc="using:SPTInstaller.CustomControls">
<Design.PreviewWith>
<StackPanel Spacing="5" Background="{StaticResource AKI_Background_Dark}">
<Button Classes="icon" x:Name="testBtn">
<Path Data="{StaticResource Bug}"
Fill="{Binding ElementName=testBtn, Path=Foreground}" />
</Button>
<TextBox Text="Some cool text here" Margin="5"/>
<TextBox Watermark="This is a watermark" Margin="5"/>
</StackPanel>
</Design.PreviewWith>
<TextBox Text="Some cool text here" Margin="5" />
<TextBox Watermark="This is a watermark" Margin="5" />
</StackPanel>
</Design.PreviewWith>
<!-- Add Styles Here -->
<!-- Add Styles Here -->
<!-- TitleBar Styles -->
<Style Selector="cc|TitleBar">
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
<Setter Property="ButtonForeground" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<!-- TitleBar Styles -->
<Style Selector="cc|TitleBar">
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}" />
<Setter Property="ButtonForeground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="cc|TitleBar.versiontag">
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="BorderThickness" Value="0 0 0 2"/>
</Style>
<Style Selector="cc|TitleBar.versiontag">
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="0 0 0 2" />
</Style>
<!-- TextBox Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml -->
<Style Selector="TextBox">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<!-- TextBox Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml -->
<Style Selector="TextBox">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
</Style>
<Style Selector="TextBox:focus">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="TextBox:focus">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
</Style>
<Style Selector="TextBox:pointerover">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="TextBox:pointerover">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
</Style>
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="DimGray"/>
</Style>
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="DimGray" />
</Style>
<Style Selector="TextBox:pointerover /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="DimGray"/>
</Style>
<Style
Selector="TextBox:pointerover /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="DimGray" />
</Style>
<Style Selector="TextBox:focus /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="DimGray"/>
</Style>
<Style
Selector="TextBox:focus /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="DimGray" />
</Style>
<Style Selector="TextBox /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="White"/>
</Style>
<Style
Selector="TextBox /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<!-- TextBlock Styles -->
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}" />
</Style>
<!-- Label Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Label.xaml -->
<Style Selector="Label">
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
</Style>
<!-- Label Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Label.xaml -->
<Style Selector="Label">
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}" />
</Style>
<Style Selector="Label.yellow">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
</Style>
<Style Selector="Label.yellow">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
</Style>
<Style Selector="Label.dark">
<Setter Property="Foreground" Value="DimGray"/>
</Style>
<Style Selector="Label.dark">
<Setter Property="Foreground" Value="DimGray" />
</Style>
<Style Selector="Label.versionMismatch">
<Setter Property="Foreground" Value="OrangeRed"/>
</Style>
<Style Selector="Label.versionMismatch">
<Setter Property="Foreground" Value="OrangeRed" />
</Style>
<!-- ProgressBar Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml -->
<Style Selector="ProgressBar">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<!-- ProgressBar Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml -->
<Style Selector="ProgressBar">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="ProgressBar.error">
<Setter Property="Foreground" Value="Red"/>
<Style.Animations>
<Animation Duration="0:0:0.5" FillMode="Forward">
<KeyFrame Cue="0%">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Value" Value="0"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Foreground" Value="Red"/>
<Setter Property="Value" Value="100"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="ProgressBar.error">
<Setter Property="Foreground" Value="Red" />
<Style.Animations>
<Animation Duration="0:0:0.5" FillMode="Forward">
<KeyFrame Cue="0%">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Value" Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Foreground" Value="Red" />
<Setter Property="Value" Value="100" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<!-- Seperator Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Separator.xaml -->
<Style Selector="Separator">
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}"/>
</Style>
<!-- Seperator Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Separator.xaml -->
<Style Selector="Separator">
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}" />
</Style>
<!-- Button Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Button.xaml -->
<Style Selector="Button">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_White}"/>
</Style>
<!-- Button Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Button.xaml -->
<Style Selector="Button">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource AKI_White}" />
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_LightGrayBlue}"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_LightGrayBlue}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_White}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_LightGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_LightGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource AKI_White}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}"/>
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}" />
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<!-- Button yellow -->
<Style Selector="Button.yellow">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<!-- Button yellow -->
<Style Selector="Button.yellow">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.yellow:pointerover">
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Button.yellow:pointerover">
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.yellow:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Gold"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style Selector="Button.yellow:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Gold" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.yellow:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="Button.yellow:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Lighter}" />
</Style>
<Style Selector="Button.yellow:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_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 -->
<Style Selector="Button.outlined">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style Selector="Button.outlined:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style Selector="Button.outlined:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<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 Link Style -->
<Style Selector="Button.link">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0 0 0 1"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0 0 0 1" />
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding $parent[Button].Content}" TextDecorations="Underline"/>
<TextBlock Text="{Binding $parent[Button].Content}" TextDecorations="Underline" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<Style Selector="Button.link:pointerover TextBlock">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
</Style>
<Style Selector="Button.link:pressed TextBlock">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="Button.link:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0 0 0 1"/>
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0 0 0 1" />
</Style>
<Style Selector="Button.link:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0 0 0 1"/>
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0 0 0 1" />
</Style>
<!-- Button outlinedTLCorner Style -->
<Style Selector="Button.outlinedTLCorner">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderThickness" Value="2 2 0 0"/>
</Style>
<Style Selector="Button.outlinedTLCorner">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="2 2 0 0" />
</Style>
<Style Selector="Button.outlinedTLCorner:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="BorderThickness" Value="2 2 0 0"/>
</Style>
<Style Selector="Button.outlinedTLCorner:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="2 2 0 0" />
</Style>
<Style Selector="Button.outlinedTLCorner:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_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 -->
<Style Selector="Button.icon">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.icon:pointerover">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
</Style>
<Style Selector="Button.icon:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="Button.icon:pressed">
<Setter Property="Foreground" Value="{StaticResource AKI_DarkGrayBlue}"></Setter>

View File

@ -5,13 +5,14 @@ namespace SPTInstaller.Behaviors;
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)
{
element.SetValue(SpanProperty, value);
}
public static bool GetSpan(AvaloniaObject element)
{
return element.GetValue(SpanProperty);

View File

@ -12,16 +12,16 @@ public class InstallController
{
public event EventHandler RecheckRequested = delegate { };
public event EventHandler<IProgressableTask> TaskChanged = delegate { };
private bool _installRunning = false;
private IPreCheck[] _preChecks { get; set; }
private IProgressableTask[] _tasks { get; set; }
public InstallController(IProgressableTask[] tasks, IPreCheck[] preChecks = null)
{
_tasks = tasks;
_preChecks = preChecks;
foreach (var check in _preChecks)
{
check.ReeevaluationRequested += (s, _) =>
@ -32,63 +32,64 @@ public class InstallController
}
Log.Information($"{preCheck.Name}: requested re-evaluation");
if (_installRunning)
{
Log.Warning("Install is running, re-evaluation denied (how did you do this?)");
return;
}
RecheckRequested?.Invoke(this, null);
};
}
}
public async Task<IResult> RunPreChecks()
{
Log.Information("-<>--<>- Running PreChecks -<>--<>-");
var requiredResults = new List<IResult>();
foreach (var check in _preChecks)
{
check.State = StatusSpinner.SpinnerState.Pending;
}
foreach (var check in _preChecks)
{
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)
{
requiredResults.Add(result);
}
}
if (requiredResults.Any(result => !result.Succeeded))
{
return Result.FromError("Some required checks have failed");
}
return Result.FromSuccess();
}
public async Task<IResult> RunTasks()
{
_installRunning = true;
Log.Information("-<>--<>- Running Installer Tasks -<>--<>-");
foreach (var task in _tasks)
{
TaskChanged?.Invoke(null, task);
var result = await task.RunAsync();
if (!result.Succeeded) return result;
}
return Result.FromSuccess("Install Complete. Happy Playing!");
}
}

View File

@ -11,17 +11,17 @@ public class InvertedProgressConverter : IValueConverter
{
return 100 - progress;
}
return value;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is int invertedProgress)
{
return 100 - invertedProgress;
}
return value;
}
}

View File

@ -9,9 +9,9 @@ public class StateSpinnerStateToColorConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value == null)
if (value == null)
return null;
if (value is not StatusSpinner.SpinnerState state)
return null;
@ -31,7 +31,7 @@ public class StateSpinnerStateToColorConverter : IValueConverter
throw new ArgumentOutOfRangeException();
}
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value;

View File

@ -3,24 +3,25 @@ using SPTInstaller.CustomControls;
using System.Globalization;
namespace SPTInstaller.Converters;
public class StatusSpinnerIsProcessingConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not StatusSpinner.SpinnerState state)
return null;
if (parameter is string parm && parm == "invert")
{
return state > 0;
}
return state <= 0;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value;
}
}
}

View File

@ -3,24 +3,25 @@ using SPTInstaller.CustomControls;
using System.Globalization;
namespace SPTInstaller.Converters;
public class StatusSpinnerIsStateConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value == null || parameter == null)
if (value == null || parameter == null)
return null;
if (value is not StatusSpinner.SpinnerState state)
return null;
if (parameter is not string stateName)
return null;
return state.ToString().ToLower() == stateName.ToLower();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value;
}
}
}

View File

@ -7,25 +7,24 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.CacheInfo">
<UserControl.Resources>
<convt:StatusSpinnerIsProcessingConverter x:Key="IsInProcessingStateConverter"/>
<convt:StatusSpinnerIsProcessingConverter x:Key="IsInProcessingStateConverter" />
</UserControl.Resources>
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,AUTO,10,AUTO,*">
<cc:StatusSpinner Grid.Column="1" 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},
Converter={StaticResource IsInProcessingStateConverter}}"
/>
Converter={StaticResource IsInProcessingStateConverter}}" />
<Path Grid.Column="1" Data="{StaticResource Cache}" Fill="DodgerBlue" Margin="0 6 0 0"
IsVisible="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
Converter={StaticResource IsInProcessingStateConverter},
ConverterParameter=invert}"
/>
ConverterParameter=invert}" />
<Label Grid.Column="3" Content="{Binding InfoText, RelativeSource={RelativeSource AncestorType=UserControl}}"
Margin="0 2 0 0"
/>
<Button Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" Content="What's this?" Classes="link" HorizontalAlignment="Center"
Command="{Binding ShowCacheDialogCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
Margin="0 2 0 0" />
<Button Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" Content="What's this?" Classes="link"
HorizontalAlignment="Center"
Command="{Binding ShowCacheDialogCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</Grid>
</UserControl>
</UserControl>

View File

@ -5,30 +5,31 @@ using SPTInstaller.CustomControls.Dialogs;
using System.Threading.Tasks;
namespace SPTInstaller.CustomControls;
public partial class CacheInfo : UserControl
{
public CacheInfo()
{
InitializeComponent();
}
public async Task ShowCacheDialogCommand() => await DialogHost.Show(new WhyCacheThoughDialog());
public string InfoText
{
get => GetValue(InfoTextProperty);
set => SetValue(InfoTextProperty, value);
}
public static readonly StyledProperty<string> InfoTextProperty =
AvaloniaProperty.Register<CacheInfo, string>(nameof(InfoText));
public StatusSpinner.SpinnerState State
{
get => GetValue(StateProperty);
set => SetValue(StateProperty, value);
}
public static readonly StyledProperty<StatusSpinner.SpinnerState> StateProperty =
AvaloniaProperty.Register<CacheInfo, StatusSpinner.SpinnerState>(nameof(State));
}
}

View File

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

View File

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

View File

@ -5,24 +5,21 @@
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300"
x:Class="SPTInstaller.CustomControls.Dialogs.ConfirmationDialog"
MinWidth="300" MinHeight="100"
MinWidth="300" MinHeight="100"
MaxWidth="600" MaxHeight="300">
<Grid RowDefinitions="10,AUTO,*,AUTO,10" ColumnDefinitions="10,*,AUTO,10,AUTO,10"
Background="{StaticResource AKI_Background_Light}">
<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}}"
TextWrapping="Wrap"
/>
TextWrapping="Wrap" />
<Button Content="No" Grid.Row="3" Grid.Column="2"
Width="50" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}"
CommandParameter="False"
Classes="yellow"
/>
Classes="yellow" />
<Button Content="Yes" Grid.Row="3" Grid.Column="4"
Width="50" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}"
CommandParameter="True"
/>
CommandParameter="True" />
</Grid>
</UserControl>
</UserControl>

View File

@ -2,21 +2,22 @@
using Avalonia.Controls;
namespace SPTInstaller.CustomControls.Dialogs;
public partial class ConfirmationDialog : UserControl
{
public ConfirmationDialog(string message)
{
InitializeComponent();
Message = message;
}
public string Message
{
get => GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public static readonly StyledProperty<string> MessageProperty =
AvaloniaProperty.Register<ConfirmationDialog, string>(nameof(Message));
}
}

View File

@ -9,8 +9,7 @@
<Grid RowDefinitions="AUTO,AUTO,AUTO,*,AUTO" ColumnDefinitions="*,AUTO, AUTO"
Background="{StaticResource AKI_Background_Light}">
<Label Content="What is the installer cache for?" FontSize="20"
Foreground="{StaticResource AKI_Brush_Yellow}"
/>
Foreground="{StaticResource AKI_Brush_Yellow}" />
<TextBlock Grid.Row="1" Grid.ColumnSpan="2" TextWrapping="Wrap" xml:space="preserve">
The installer cache is used to ensure you don't re-download large files that you've already downloaded before.
<Span Foreground="red">You should only delete the cache folder if</Span>
@ -19,31 +18,27 @@ The installer cache is used to ensure you don't re-download large files that you
- You are not planning on installing SPT again any time soon
If possible, you should leave the cache in place to avoid uneccessary, lengthy downloads.
It also helps us prevent extra traffic to our limited download mirrors. Every bit helps <Span Foreground="red" FontSize="25">♥️</Span>
It also helps us prevent extra traffic to our limited download mirrors. Every bit helps <Span Foreground="red"
FontSize="25">♥️</Span>
</TextBlock>
<Label Grid.Row="2" Content="You can find the cache folder here"
/>
<Button Grid.Row="3" Grid.ColumnSpan="2" Content="{Binding Source={x:Static helpers:DownloadCacheHelper.CachePath}}"
<Label Grid.Row="2" Content="You can find the cache folder here" />
<Button Grid.Row="3" Grid.ColumnSpan="2"
Content="{Binding Source={x:Static helpers:DownloadCacheHelper.CachePath}}"
Classes="link"
Margin="0 10"
IsVisible="{Binding CacheExists, RelativeSource={RelativeSource AncestorType=UserControl}}"
Command="{Binding OpenCacheFolder, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
Command="{Binding OpenCacheFolder, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<Label Grid.Row="3" Content="No cache folder exists"
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}"
/>
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}" />
<Button Grid.Row="4" Grid.Column="1" Content="Move Downloaded Patcher" Margin="0 0 10 0"
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=MoveDownloadsPatcherToCache}"
/>
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=MoveDownloadsPatcherToCache}" />
<Button Grid.Row="4" Grid.Column="2" Content="Close" Classes="yellow"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}"
/>
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}" />
</Grid>
</UserControl>
</UserControl>

View File

@ -6,51 +6,54 @@ using Avalonia.Controls;
using Serilog;
namespace SPTInstaller.CustomControls.Dialogs;
public partial class WhyCacheThoughDialog : UserControl
{
private int _movePatcherState = 0;
private FileInfo? _foundPatcher;
public WhyCacheThoughDialog()
{
InitializeComponent();
}
public static readonly StyledProperty<string> AdditionalInfoProperty =
AvaloniaProperty.Register<WhyCacheThoughDialog, string>(nameof(AdditionalInfo));
public string AdditionalInfo
{
get => GetValue(AdditionalInfoProperty);
set => SetValue(AdditionalInfoProperty, value);
}
public static readonly StyledProperty<string> AdditionalInfoColorProperty =
AvaloniaProperty.Register<WhyCacheThoughDialog, string>(nameof(AdditionalInfoColor));
public string AdditionalInfoColor
{
get => GetValue(AdditionalInfoColorProperty);
set => SetValue(AdditionalInfoColorProperty, value);
}
public bool CacheExists => Directory.Exists(DownloadCacheHelper.CachePath);
public void OpenCacheFolder()
{
if (!CacheExists)
return;
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,
Verb = "open"
});
}
public void MoveDownloadsPatcherToCache()
{
switch (_movePatcherState)
@ -58,9 +61,9 @@ public partial class WhyCacheThoughDialog : UserControl
case 0:
var downloadsPath =
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
var downloadsFolder = new DirectoryInfo(downloadsPath);
if (!downloadsFolder.Exists)
{
var message = "Could not get downloads folder :(";
@ -70,21 +73,23 @@ public partial class WhyCacheThoughDialog : UserControl
_movePatcherState = -1;
return;
}
_foundPatcher = downloadsFolder.GetFiles("Patcher_*").OrderByDescending(p => p.CreationTime).FirstOrDefault();
_foundPatcher = downloadsFolder.GetFiles("Patcher_*").OrderByDescending(p => p.CreationTime)
.FirstOrDefault();
if (_foundPatcher == null || !_foundPatcher.Exists)
{
var message = "Could not find a patcher file in your downloads folder";
Log.Warning($"[MV_0] {message}");
AdditionalInfo = message;
AdditionalInfoColor = "red";
return;
}
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-"}";
AdditionalInfo =
$"Click again to move the below patcher file to the cache folder\n{_foundPatcher?.Name ?? "-SOMETHING WENT WRONG-"}";
AdditionalInfoColor = "#FFC107";
_movePatcherState = 1;
break;
@ -104,10 +109,11 @@ public partial class WhyCacheThoughDialog : UserControl
AdditionalInfoColor = "red";
Log.Error(ex, "Failed to move downloaded patcher file into cache");
}
break;
default:
Log.Error("[MV_ ] Move state is broken :(");
break;
}
}
}
}

View File

@ -10,66 +10,67 @@ public class DistributedSpacePanel : Panel
protected override Size MeasureOverride(Size availableSize)
{
var children = Children;
for (int i = 0; i < children.Count; i++)
{
// measure child objects so we can use their desired size in the arrange override
var child = children[i];
child.Measure(availableSize);
}
// we want to use all available space
return availableSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
var children = Children;
Rect rcChild = new Rect(finalSize);
double previousChildSize = 0.0;
// get child objects that don't want to span the entire control
var nonSpanningChildren = children.Where(x => x.GetValue(SpanBehavior.SpanProperty) == false).ToList();
// get the total height off all non-spanning child objects
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
// except the last one, since it needs no space after it
var spacing = (finalSize.Height - totalChildHeight) / (nonSpanningChildren.Count - 1);
for (int i = 0; i < children.Count; i++)
{
var child = children[i];
var spanChild = child.GetValue(SpanBehavior.SpanProperty);
if (spanChild)
{
// move any spanning children to the top of the array to push them behind the other controls (visually)
children.Move(i, 0);
rcChild = rcChild.WithY(0)
.WithX(0)
.WithHeight(finalSize.Height)
.WithWidth(finalSize.Width);
child.Arrange(rcChild);
continue;
};
}
;
rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
previousChildSize = child.DesiredSize.Height;
rcChild = rcChild.WithHeight(previousChildSize)
.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
previousChildSize += spacing;
child.Arrange(rcChild);
}
return finalSize;
}
}

View File

@ -6,18 +6,17 @@
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"/>
<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}}"
/>
IsVisible="{Binding HasSelection, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={x:Static BoolConverters.Not}}" />
<ItemsControl ItemsSource="{Binding PreChecks}" VerticalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@ -27,22 +26,21 @@
<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"/>
<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"/>
FontSize="20" />
<Rectangle Height="1" Fill="Gray" Margin="0 10" />
<TextBlock Text="{Binding PreCheckDetails}"
TextWrapping="Wrap"
/>
TextWrapping="Wrap" />
</StackPanel>
<Button Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Classes="yellow"
IsVisible="{Binding ActionButtonIsVisible}"
CornerRadius="15"
@ -50,12 +48,11 @@
Command="{Binding ActionButtonCommand}"
Content="{Binding ActionButtonText}"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
HorizontalAlignment="Stretch"
/>
HorizontalAlignment="Stretch" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Panel>
</UserControl>
</UserControl>

View File

@ -11,19 +11,19 @@ public partial class PreCheckDetails : UserControl
{
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);

View File

@ -5,59 +5,56 @@
xmlns:cc="using:SPTInstaller.CustomControls"
xmlns:convt="using:SPTInstaller.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.PreCheckItem"
>
x:Class="SPTInstaller.CustomControls.PreCheckItem">
<UserControl.Resources>
<convt:StatusSpinnerIsStateConverter x:Key="IsStateConverter"/>
<convt:StatusSpinnerIsStateConverter x:Key="IsStateConverter" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Label.bold">
<Setter Property="FontWeight" Value="Bold"/>
</Style>
<UserControl.Styles>
<Style Selector="Label.bold">
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="Button.selectable">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.selectable:pointerover">
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.selectable:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.selectable:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="Button.selected">
<Setter Property="Background" Value="{StaticResource AKI_Brush_LightGrayBlue}" />
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderThickness" Value="1" />
</Style>
</UserControl.Styles>
</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}}"
>
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},
<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}"
/>
ConverterParameter=Running}" />
</Grid>
</Button>
</UserControl>
</UserControl>

View File

@ -10,47 +10,47 @@ public partial class PreCheckItem : UserControl
{
InitializeComponent();
}
public string PreCheckName
{
get => GetValue(PreCheckNameProperty);
set => SetValue(PreCheckNameProperty, value);
}
public static readonly StyledProperty<string> PreCheckNameProperty =
AvaloniaProperty.Register<PreCheckItem, string>(nameof(PreCheckName));
public bool IsRequired
{
get => GetValue(IsRequiredProperty);
set => SetValue(IsRequiredProperty, value);
}
public static readonly StyledProperty<bool> IsRequiredProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsRequired));
public StatusSpinner.SpinnerState State
{
get => GetValue(StateProperty);
set => SetValue(StateProperty, value);
}
public static readonly StyledProperty<StatusSpinner.SpinnerState> StateProperty =
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);

View File

@ -2,78 +2,83 @@
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:vm="using:SPTInstaller.ViewModels"
xmlns:vm="using:SPTInstaller.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.ProgressableTaskItem">
<UserControl.Styles>
<!-- Ellipse Styles -->
<Style Selector="Ellipse">
<Setter Property="Stroke" Value="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<Setter Property="Margin" Value="7 0"/>
</Style>
<Style Selector="Ellipse.completed">
<Setter Property="Stroke" Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Style>
<Style Selector="Ellipse.running">
<Setter Property="Stroke" Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<Setter Property="Margin" Value="7 3"/>
<Style.Animations>
<Animation Duration="0:0:1" PlaybackDirection="Alternate" IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleX" Value="1"/>
<Setter Property="ScaleTransform.ScaleY" Value="1"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="ScaleTransform.ScaleX" Value="1.2"/>
<Setter Property="ScaleTransform.ScaleY" Value="1.2"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Ellipse.centerRunning">
<Setter Property="Fill" Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Style>
<Style Selector="Ellipse.centerCompleted">
<Setter Property="Fill" Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Style>
<UserControl.Styles>
<!-- Ellipse Styles -->
<Style Selector="Ellipse">
<Setter Property="Stroke"
Value="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<Setter Property="Margin" Value="7 0" />
</Style>
<Style Selector="Ellipse.completed">
<Setter Property="Stroke"
Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</Style>
<Style Selector="Ellipse.running">
<Setter Property="Stroke"
Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<Setter Property="Margin" Value="7 3" />
<Style.Animations>
<Animation Duration="0:0:1" PlaybackDirection="Alternate" IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleX" Value="1" />
<Setter Property="ScaleTransform.ScaleY" Value="1" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="ScaleTransform.ScaleX" Value="1.2" />
<Setter Property="ScaleTransform.ScaleY" Value="1.2" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Ellipse.centerRunning">
<Setter Property="Fill"
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 -->
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{Binding PendingColor, RelativeSource={RelativeSource
AncestorType=UserControl}}"/>
</Style>
<Style Selector="TextBlock.completed">
<Setter Property="Foreground" Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Style>
<Style Selector="TextBlock.running">
<Setter Property="Foreground" Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="AUTO, *">
<Ellipse Height="30" Width="30"
StrokeThickness="4"
Fill="{StaticResource AKI_Background_Dark}"
HorizontalAlignment="Left"
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.completed="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
<!-- Label Styles -->
<Style Selector="TextBlock">
<Setter Property="Foreground"
Value="{Binding PendingColor, RelativeSource={RelativeSource
AncestorType=UserControl}}" />
</Style>
<Style Selector="TextBlock.completed">
<Setter Property="Foreground"
Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</Style>
<Style Selector="TextBlock.running">
<Setter Property="Foreground"
Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="AUTO, *">
<Ellipse Height="20" Width="20"
Classes.centerRunning="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.centerCompleted="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
<TextBlock Grid.Column="1"
Text="{Binding TaskName, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.completed="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
FontWeight="SemiBold"
FontSize="15"
TextWrapping="Wrap"
VerticalAlignment="Center"
HorizontalAlignment="Left"
/>
</Grid>
</UserControl>
<Ellipse Height="30" Width="30"
StrokeThickness="4"
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"
Classes.centerRunning="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.centerCompleted="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<TextBlock Grid.Column="1"
Text="{Binding TaskName, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.completed="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
FontWeight="SemiBold"
FontSize="15"
TextWrapping="Wrap"
VerticalAlignment="Center"
HorizontalAlignment="Left" />
</Grid>
</UserControl>

View File

@ -10,67 +10,67 @@ public partial class ProgressableTaskItem : UserControl
{
InitializeComponent();
}
public string TaskId
{
get => GetValue(TaskIdProperty);
set => SetValue(TaskIdProperty, value);
}
public static readonly StyledProperty<string> TaskIdProperty =
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskId));
public string TaskName
{
get => GetValue(TaskNameProperty);
set => SetValue(TaskNameProperty, value);
}
public static readonly StyledProperty<string> TaskNameProperty =
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskName));
public bool IsCompleted
{
get => GetValue(IsCompletedProperty);
set => SetValue(IsCompletedProperty, value);
}
public static readonly StyledProperty<bool> IsCompletedProperty =
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsCompleted));
public bool IsRunning
{
get => GetValue(IsRunningProperty);
set => SetValue(IsRunningProperty, value);
}
public static readonly StyledProperty<bool> IsRunningProperty =
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsRunning));
public IBrush PendingColor
{
get => GetValue(PendingColorProperty);
set => SetValue(PendingColorProperty, value);
}
public static readonly StyledProperty<IBrush> PendingColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
public IBrush RunningColor
{
get => GetValue(RunningColorProperty);
set => SetValue(RunningColorProperty, value);
}
public static readonly StyledProperty<IBrush> RunningColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
public IBrush CompletedColor
{
get => GetValue(CompletedColorProperty);
set => SetValue(CompletedColorProperty, value);
}
public static readonly StyledProperty<IBrush> CompletedColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
}

View File

@ -2,15 +2,14 @@
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"
xmlns:bh="using:SPTInstaller.Behaviors"
xmlns:convt="using:SPTInstaller.Converters"
xmlns:cc="using:SPTInstaller.CustomControls"
xmlns:bh="using:SPTInstaller.Behaviors"
xmlns:convt="using:SPTInstaller.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.ProgressableTaskList"
>
<UserControl.Resources>
<convt:InvertedProgressConverter x:Key="invtProgressConvt"/>
</UserControl.Resources>
x:Class="SPTInstaller.CustomControls.ProgressableTaskList">
<UserControl.Resources>
<convt:InvertedProgressConverter x:Key="invtProgressConvt" />
</UserControl.Resources>
<Grid>
<ProgressBar Orientation="Vertical"
@ -19,13 +18,13 @@
Foreground="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Margin="25 20"
/>
<ItemsControl Name="itemsControl" ItemsSource="{Binding Tasks, RelativeSource={RelativeSource AncestorType=UserControl}}"
Padding="5">
Margin="25 20" />
<ItemsControl Name="itemsControl"
ItemsSource="{Binding Tasks, RelativeSource={RelativeSource AncestorType=UserControl}}"
Padding="5">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cc:DistributedSpacePanel/>
<cc:DistributedSpacePanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
@ -36,10 +35,9 @@
IsCompleted="{Binding IsCompleted}"
PendingColor="{Binding PendingColor, 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>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>
</UserControl>

View File

@ -15,76 +15,77 @@ public partial class ProgressableTaskList : UserControl
public ProgressableTaskList()
{
InitializeComponent();
this.AttachedToVisualTree += ProgressableTaskList_AttachedToVisualTree;
}
private int _taskProgress;
public int TaskProgress
{
get => _taskProgress;
set => SetAndRaise(ProgressableTaskList.TaskProgressProperty, ref _taskProgress, value);
}
public static readonly DirectProperty<ProgressableTaskList, int> TaskProgressProperty =
AvaloniaProperty.RegisterDirect<ProgressableTaskList, int>(nameof(TaskProgress), o => o.TaskProgress);
public ObservableCollection<InstallerTaskBase> Tasks
{
get => GetValue(TasksProperty);
set => SetValue(TasksProperty, value);
}
public static readonly StyledProperty<ObservableCollection<InstallerTaskBase>> TasksProperty =
AvaloniaProperty.Register<ProgressableTaskList, ObservableCollection<InstallerTaskBase>>(nameof(Tasks));
public IBrush PendingColor
{
get => GetValue(PendingColorProperty);
set => SetValue(PendingColorProperty, value);
}
public static readonly StyledProperty<IBrush> PendingColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
public IBrush RunningColor
{
get => GetValue(RunningColorProperty);
set => SetValue(RunningColorProperty, value);
}
public static readonly StyledProperty<IBrush> RunningColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
public IBrush CompletedColor
{
get => GetValue(CompletedColorProperty);
set => SetValue(CompletedColorProperty, value);
}
public static readonly StyledProperty<IBrush> CompletedColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
private void UpdateTaskProgress()
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
var completedTasks = Tasks.Where(x => x.IsCompleted == true).Count();
var progress = (int)Math.Floor((double)completedTasks / (Tasks.Count - 1) * 100);
for(; TaskProgress < progress;)
for (; TaskProgress < progress;)
{
TaskProgress += 1;
await Task.Delay(1);
}
});
}
private void ProgressableTaskList_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (Tasks == null) return;
foreach (var task in Tasks)
{
task.WhenPropertyChanged(x => x.IsCompleted)

View File

@ -4,56 +4,55 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:convt="using:SPTInstaller.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.StatusSpinner"
>
x:Class="SPTInstaller.CustomControls.StatusSpinner">
<UserControl.Resources>
<convt:StatusSpinnerIsStateConverter x:Key="IsStateConverter"/>
<convt:StatusSpinnerIsProcessingConverter x:Key="IsInProcessingStateConverter"/>
<convt:StatusSpinnerIsStateConverter x:Key="IsStateConverter" />
<convt:StatusSpinnerIsProcessingConverter x:Key="IsInProcessingStateConverter" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Arc.running">
<Setter Property="Stroke" Value="DodgerBlue"/>
<Setter Property="Stroke" Value="DodgerBlue" />
</Style>
<Style Selector="Path.ok">
<Setter Property="Data" Value="{StaticResource CircledCheck}"/>
<Setter Property="Fill" Value="Green"/>
<Setter Property="Data" Value="{StaticResource CircledCheck}" />
<Setter Property="Fill" Value="Green" />
</Style>
<Style Selector="Path.warning">
<Setter Property="Data" Value="{StaticResource CircledWarn}"/>
<Setter Property="Fill" Value="Goldenrod"/>
<Setter Property="Data" Value="{StaticResource CircledWarn}" />
<Setter Property="Fill" Value="Goldenrod" />
</Style>
<Style Selector="Path.error">
<Setter Property="Data" Value="{StaticResource CircledX}"/>
<Setter Property="Fill" Value="Red"/>
<Setter Property="Data" Value="{StaticResource CircledX}" />
<Setter Property="Fill" Value="Red" />
</Style>
<Style Selector="Arc">
<Setter Property="Stroke" Value="Gray"/>
<Setter Property="IsVisible" Value="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
Converter={StaticResource ResourceKey=IsInProcessingStateConverter}}"/>
<Setter Property="Stroke" Value="Gray" />
<Setter Property="IsVisible"
Value="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
Converter={StaticResource ResourceKey=IsInProcessingStateConverter}}" />
<Style.Animations>
<Animation Duration="0:0:1" IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="RotateTransform.Angle" Value="0"/>
<Setter Property="RotateTransform.Angle" Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="RotateTransform.Angle" Value="360"/>
<Setter Property="RotateTransform.Angle" Value="360" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</UserControl.Styles>
<Grid>
<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}">
<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"
Classes.ok="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
Converter={StaticResource ResourceKey=IsStateConverter},
@ -63,15 +62,13 @@
ConverterParameter=Warning}"
Classes.error="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
Converter={StaticResource ResourceKey=IsStateConverter},
ConverterParameter=Error}"
/>
ConverterParameter=Error}" />
</Canvas>
<Arc StartAngle="280" SweepAngle="80" Margin="0 3 0 0" StrokeThickness="3"
Width="20" Height="20" VerticalAlignment="Top"
Classes.running="{Binding State, RelativeSource={RelativeSource AncestorType=UserControl},
Converter={StaticResource ResourceKey=IsStateConverter},
ConverterParameter=Running}"
/>
ConverterParameter=Running}" />
</Grid>
</UserControl>
</UserControl>

View File

@ -10,22 +10,22 @@ public partial class StatusSpinner : ReactiveUserControl<UserControl>
{
Pending = -1,
Running = 0,
OK = 1,
OK = 1,
Warning = 2,
Error = 3,
Error = 3,
}
public StatusSpinner()
{
InitializeComponent();
}
public SpinnerState State
{
get => GetValue(StateProperty);
set => SetValue(StateProperty, value);
}
public static readonly StyledProperty<SpinnerState> StateProperty =
AvaloniaProperty.Register<StatusSpinner, SpinnerState>(nameof(State));
}
}

View File

@ -4,37 +4,33 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.TaskDetails">
<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}}"
/>
<Grid RowDefinitions="10,*,AUTO,10,AUTO,*,AUTO,10" ColumnDefinitions="10,*,10">
<TextBlock Grid.Column="1" Grid.Row="4"
Foreground="Gainsboro"
HorizontalAlignment="Center"
FontSize="12"
Text="{Binding Details, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextTrimming="CharacterEllipsis"
TextWrapping="Wrap"
MaxLines="3"
/>
<Label Grid.Column="1" Grid.Row="2"
HorizontalAlignment="Center"
FontSize="15"
FontWeight="SemiBold"
Content="{Binding Message, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<Grid Grid.Column="1" Grid.Row="6" ColumnDefinitions="*,AUTO">
<ProgressBar IsVisible="{Binding ShowProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
Value="{Binding Progress, RelativeSource={RelativeSource AncestorType=UserControl}}"
HorizontalAlignment="Stretch"
IsIndeterminate="{Binding IndeterminateProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
<TextBlock Grid.Column="1" Grid.Row="4"
Foreground="Gainsboro"
HorizontalAlignment="Center"
FontSize="12"
Text="{Binding Details, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextTrimming="CharacterEllipsis"
TextWrapping="Wrap"
MaxLines="3" />
<Label Grid.Column="1"
Content="{Binding Progress, RelativeSource={RelativeSource AncestorType=UserControl}, StringFormat='{}{0}%'}"
IsVisible="{Binding !IndeterminateProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
</Grid>
</Grid>
</UserControl>
<Grid Grid.Column="1" Grid.Row="6" ColumnDefinitions="*,AUTO">
<ProgressBar IsVisible="{Binding ShowProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
Value="{Binding Progress, RelativeSource={RelativeSource AncestorType=UserControl}}"
HorizontalAlignment="Stretch"
IsIndeterminate="{Binding IndeterminateProgress, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<Label Grid.Column="1"
Content="{Binding Progress, RelativeSource={RelativeSource AncestorType=UserControl}, StringFormat='{}{0}%'}"
IsVisible="{Binding !IndeterminateProgress, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</Grid>
</Grid>
</UserControl>

View File

@ -9,49 +9,49 @@ public partial class TaskDetails : UserControl
{
InitializeComponent();
}
public string Message
{
get => GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public static readonly StyledProperty<string> MessageProperty =
AvaloniaProperty.Register<TaskDetails, string>(nameof(Message));
public string Details
{
get => GetValue(DetailsProperty);
set => SetValue(DetailsProperty, value);
}
public static readonly StyledProperty<string> DetailsProperty =
AvaloniaProperty.Register<TaskDetails, string>(nameof(Details));
public int Progress
{
get => GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}
public static readonly StyledProperty<int> ProgressProperty =
AvaloniaProperty.Register<TaskDetails, int>(nameof(Progress));
public bool ShowProgress
{
get => GetValue(ShowProgressProperty);
set => SetValue(ShowProgressProperty, value);
}
public static readonly StyledProperty<bool> ShowProgressProperty =
AvaloniaProperty.Register<TaskDetails, bool>(nameof(ShowProgress));
public bool IndeterminateProgress
{
get => GetValue(IndeterminateProgressProperty);
set => SetValue(IndeterminateProgressProperty, value);
}
public static readonly StyledProperty<bool> IndeterminateProgressProperty =
AvaloniaProperty.Register<TaskDetails, bool>(nameof(IndeterminateProgress));
}

View File

@ -5,72 +5,68 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.TitleBar">
<Grid ColumnDefinitions="AUTO,*,AUTO,AUTO">
<Grid ColumnDefinitions="AUTO,*,AUTO,AUTO">
<Rectangle Grid.ColumnSpan="6" IsHitTestVisible="False"
Fill="{Binding Background, RelativeSource={
RelativeSource AncestorType=UserControl}}"
/>
<Rectangle Grid.ColumnSpan="6" IsHitTestVisible="False"
Fill="{Binding Background, RelativeSource={
RelativeSource AncestorType=UserControl}}" />
<Label Content="{Binding Title, RelativeSource={
<Label Content="{Binding Title, RelativeSource={
RelativeSource AncestorType=UserControl}}"
IsHitTestVisible="False"
Foreground="{Binding Foreground, RelativeSource={
IsHitTestVisible="False"
Foreground="{Binding Foreground, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Background="Transparent"
VerticalContentAlignment="Center"
/>
Background="Transparent"
VerticalContentAlignment="Center" />
<!-- Minimize (-) Button -->
<Button Content="&#xE949;" Grid.Column="2"
Foreground="{Binding ButtonForeground, RelativeSource={
<!-- Minimize (-) Button -->
<Button Content="&#xE949;" Grid.Column="2"
Foreground="{Binding ButtonForeground, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Command="{Binding MinButtonCommand, RelativeSource={
Command="{Binding MinButtonCommand, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Background="Transparent"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
VerticalAlignment="Stretch"
FontFamily="Segoe MDL2 Assets"
CornerRadius="0"
Width="35"
>
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}"/>
</Style>
</Button.Styles>
</Button>
Background="Transparent"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
VerticalAlignment="Stretch"
FontFamily="Segoe MDL2 Assets"
CornerRadius="0"
Width="35">
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}" />
</Style>
</Button.Styles>
</Button>
<!-- Close (X) Button -->
<Button Content="&#xE106;" Grid.Column="3"
Foreground="{Binding ButtonForeground, RelativeSource={
<!-- Close (X) Button -->
<Button Content="&#xE106;" Grid.Column="3"
Foreground="{Binding ButtonForeground, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Command="{Binding XButtonCommand, RelativeSource={
Command="{Binding XButtonCommand, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Background="Transparent"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
VerticalAlignment="Stretch"
FontFamily="Segoe MDL2 Assets"
CornerRadius="0"
Width="35"
>
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="IndianRed"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="Crimson"/>
</Style>
</Button.Styles>
</Button>
Background="Transparent"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
VerticalAlignment="Stretch"
FontFamily="Segoe MDL2 Assets"
CornerRadius="0"
Width="35">
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="IndianRed" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="Crimson" />
</Style>
</Button.Styles>
</Button>
</Grid>
</Grid>
</UserControl>
</UserControl>

View File

@ -12,62 +12,62 @@ public partial class TitleBar : UserControl
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public static readonly StyledProperty<string> TitleProperty =
AvaloniaProperty.Register<TitleBar, string>(nameof(Title));
public string Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly StyledProperty<IBrush> ButtonForegroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(ButtonForeground));
public IBrush ButtonForeground
{
get => GetValue(ButtonForegroundProperty);
set => SetValue(ButtonForegroundProperty, value);
}
public static new readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Foreground));
public new IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public static new readonly StyledProperty<IBrush> BackgroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Background));
public new IBrush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
//Close Button Command (X Button) Property
public static readonly StyledProperty<ICommand> XButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(XButtonCommand));
public ICommand XButtonCommand
{
get => GetValue(XButtonCommandProperty);
set => SetValue(XButtonCommandProperty, value);
}
//Minimize Button Command (- Button) Property
public static readonly StyledProperty<ICommand> MinButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(MinButtonCommand));
public ICommand MinButtonCommand
{
get => GetValue(MinButtonCommandProperty);

View File

@ -5,43 +5,44 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.UpdateButton">
<UserControl.Styles>
<StyleInclude Source="../Assets/Styles.axaml"/>
<StyleInclude Source="../Assets/Styles.axaml" />
</UserControl.Styles>
<Panel>
<StackPanel Orientation="Horizontal">
<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>
<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>
<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"/>
<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>
<Button HorizontalAlignment="Center" Content="What's new?" Classes="link"
Command="{Binding WhatsNewCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</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>
</Panel>
</UserControl>
</UserControl>

View File

@ -10,73 +10,85 @@ public partial class UpdateButton : UserControl
{
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 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 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 static readonly StyledProperty<ICommand> UpdateCommandProperty =
AvaloniaProperty.Register<UpdateButton, ICommand>(
"UpdateCommand");
public ICommand UpdateCommand
{
get => GetValue(UpdateCommandProperty);
set => SetValue(UpdateCommandProperty, value);
}
public static readonly StyledProperty<ICommand> WhatsNewCommandProperty =
AvaloniaProperty.Register<UpdateButton, ICommand>("WhatsNewCommand");
public ICommand WhatsNewCommand
{
get => GetValue(WhatsNewCommandProperty);
set => SetValue(WhatsNewCommandProperty, value);
}
public static readonly StyledProperty<bool> UpdatingProperty = AvaloniaProperty.Register<UpdateButton, bool>(
"Updating");
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);

View File

@ -1,65 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.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>

View File

@ -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));
}

View File

@ -1,3 +1,4 @@
// Global using directives
global using System;
global using System.IO;

View File

@ -8,32 +8,32 @@ public static class DirectorySizeHelper
// SizeSuffix implementation found here:
// https://stackoverflow.com/a/14488941
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)
{
if (decimalPlaces < 0)
{
throw new ArgumentOutOfRangeException("decimalPlaces");
}
if (value < 0)
{
return "-" + SizeSuffix(-value, decimalPlaces);
}
if (value == 0)
{
return string.Format("{0:n" + decimalPlaces + "} bytes", 0);
}
// mag is 0 for bytes, 1 for KB, 2, for MB, etc.
int mag = (int)Math.Log(value, 1024);
// 1L << (mag * 10) == 2 ^ (10 * mag)
// [i.e. the number of bytes in the unit corresponding to mag]
decimal adjustedSize = (decimal)value / (1L << (mag * 10));
// make adjustment when the value is large enough that
// it would round up to 1000 or more
if (Math.Round(adjustedSize, decimalPlaces) >= 1000)
@ -41,12 +41,12 @@ public static class DirectorySizeHelper
mag += 1;
adjustedSize /= 1024;
}
return string.Format("{0:n" + decimalPlaces + "} {1}",
adjustedSize,
SizeSuffixes[mag]);
}
/// <summary>
/// Gets the size of a directory in bytes
/// </summary>

View File

@ -7,12 +7,16 @@ namespace SPTInstaller.Helpers;
public static class DownloadCacheHelper
{
private static HttpClient _httpClient = new() { Timeout = TimeSpan.FromHours(1) };
public static string CachePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "spt-installer/cache");
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 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()
{
if (!Directory.Exists(CachePath))
@ -21,24 +25,24 @@ public static class DownloadCacheHelper
Log.Information(message);
return message;
}
var cacheDir = new DirectoryInfo(CachePath);
var cacheSize = DirectorySizeHelper.GetSizeOfDirectory(cacheDir);
if (cacheSize == -1)
{
var message = "An error occurred while getting the cache size :(";
Log.Error(message);
return message;
}
if (cacheSize == 0)
return "Empty";
return DirectorySizeHelper.SizeSuffix(cacheSize);
}
/// <summary>
/// Check if a file in the cache already exists
/// </summary>
@ -46,42 +50,42 @@ public static class DownloadCacheHelper
/// <param name="expectedHash">The expected hash of the file in the cache</param>
/// <param name="cachedFile">The file found in the cache; null if no file is found</param>
/// <returns>True if the file is in the cache and its hash matches the expected hash, otherwise false</returns>
public static bool CheckCache(string fileName, string expectedHash, out FileInfo cachedFile)
public static bool CheckCache(string fileName, string expectedHash, out FileInfo cachedFile)
=> CheckCache(new FileInfo(Path.Join(CachePath, fileName)), expectedHash, out cachedFile);
private static bool CheckCache(FileInfo cacheFile, string expectedHash, out FileInfo fileInCache)
{
fileInCache = cacheFile;
try
{
cacheFile.Refresh();
Directory.CreateDirectory(CachePath);
if (!cacheFile.Exists || expectedHash == null)
{
Log.Information($"{cacheFile.Name} {(cacheFile.Exists ? "is in cache" : "NOT in cache")}");
Log.Information($"Expected hash: {(expectedHash == null ? "not provided" : expectedHash)}");
return false;
}
if (FileHashHelper.CheckHash(cacheFile, expectedHash))
{
fileInCache = cacheFile;
Log.Information("Hashes MATCH");
return true;
}
Log.Warning("Hashes DO NOT MATCH");
return false;
}
catch(Exception ex)
catch (Exception ex)
{
Log.Error(ex, "Something went wrong during hashing");
return false;
}
}
/// <summary>
/// Download a file to the cache folder
/// </summary>
@ -90,28 +94,29 @@ public static class DownloadCacheHelper
/// <param name="progress">A provider for progress updates</param>
/// <returns>A <see cref="FileInfo"/> object of the cached file</returns>
/// <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);
var outputFile = new FileInfo(Path.Join(CachePath, outputFileName));
try
{
if (outputFile.Exists)
outputFile.Delete();
// Use the provided extension method
using (var file = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None))
await _httpClient.DownloadDataAsync(targetLink, file, progress);
outputFile.Refresh();
if (!outputFile.Exists)
{
Log.Error("Failed to download file from url: {name} :: {url}", outputFileName, targetLink);
return null;
}
return outputFile;
}
catch (Exception ex)
@ -120,7 +125,7 @@ public static class DownloadCacheHelper
return null;
}
}
/// <summary>
/// Download a file to the cache folder
/// </summary>
@ -132,36 +137,36 @@ public static class DownloadCacheHelper
{
Directory.CreateDirectory(CachePath);
var outputFile = new FileInfo(Path.Join(CachePath, outputFileName));
try
{
if (outputFile.Exists)
outputFile.Delete();
using var patcherFileStream = outputFile.Open(FileMode.Create);
{
await downloadStream.CopyToAsync(patcherFileStream);
}
patcherFileStream.Close();
outputFile.Refresh();
if (!outputFile.Exists)
{
Log.Error("Failed to download file from stream: {name}", outputFileName);
return null;
}
return outputFile;
}
catch(Exception ex)
catch (Exception ex)
{
Log.Error(ex, "Failed to download file from stream: {fileName}", outputFileName);
return null;
}
}
/// <summary>
/// Get the file from cache or download it
/// </summary>
@ -171,13 +176,14 @@ public static class DownloadCacheHelper
/// <param name="expectedHash">The expected hash of the cached file</param>
/// <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>
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
{
if (CheckCache(fileName, expectedHash, out var cacheFile))
return cacheFile;
return await DownloadFileAsync(fileName, targetLink, progress);
}
catch (Exception ex)
@ -186,7 +192,7 @@ public static class DownloadCacheHelper
return null;
}
}
/// <summary>
/// Get the file from cache or download it
/// </summary>
@ -195,13 +201,14 @@ public static class DownloadCacheHelper
/// <param name="expectedHash">The expected hash of the cached file</param>
/// <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>
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
{
if (CheckCache(fileName, expectedHash, out var cacheFile))
return cacheFile;
return await DownloadFileAsync(fileName, fileDownloadStream);
}
catch (Exception ex)

View File

@ -17,7 +17,7 @@ public static class FileHashHelper
//
// return null;
// }
public static bool CheckHash(FileInfo file, string expectedHash)
{
using var md5Service = MD5.Create();
@ -27,9 +27,9 @@ public static class FileHashHelper
var expectedHashBytes = Convert.FromBase64String(expectedHash);
Log.Information($"Comparing Hashes :: S: {Convert.ToBase64String(sourceHash)} - E: {expectedHash}");
var matched = Enumerable.SequenceEqual(sourceHash, expectedHashBytes);
return matched;
}
}

View File

@ -16,24 +16,26 @@ public static class FileHelper
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}'");
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)
@ -42,44 +44,54 @@ public static class FileHelper
return Result.FromError(ex.Message);
}
}
private static Result IterateFiles(DirectoryInfo sourceDir, DirectoryInfo targetDir, string[] exclusions, Action<string, int> updateCallback = null)
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}'");
Log.Debug(
$"EXCLUSION FOUND :: FILE\nExclusion: '{exclusion}'\nPath: '{currentFileRelativePath}'");
break;
}
if (currentFileRelativePath.EndsWith(".bak"))
{
exclude = true;
Log.Debug($"EXCLUDING BAK FILE :: {currentFileRelativePath}");
break;
}
}
if (exclude)
continue;
var targetFile = file.FullName.Replace(sourceDir.FullName, targetDir.FullName);
Log.Debug($"COPY\nSourceDir: '{sourceDir.FullName}'\nTargetDir: '{targetDir.FullName}'\nNewPath: '{targetFile}'");
Log.Debug(
$"COPY\nSourceDir: '{sourceDir.FullName}'\nTargetDir: '{targetDir.FullName}'\nNewPath: '{targetFile}'");
File.Copy(file.FullName, targetFile, true);
processedFiles++;
}
return Result.FromSuccess();
}
catch (Exception ex)
@ -88,35 +100,37 @@ public static class FileHelper
return Result.FromError(ex.Message);
}
}
public static string GetRedactedPath(string path)
{
var nameMatched = Regex.Match(path, @".:\\[uU]sers\\(?<NAME>[^\\]+)");
if (nameMatched.Success)
{
var name = nameMatched.Groups["NAME"].Value;
return path.Replace(name, "-REDACTED-");
}
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);
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
{
var iterateDirectoriesResult = IterateDirectories(sourceDir, targetDir, exclusions ??= new string[0]);
if(!iterateDirectoriesResult.Succeeded) return iterateDirectoriesResult;
if (!iterateDirectoriesResult.Succeeded) return iterateDirectoriesResult;
var iterateFilesResult = IterateFiles(sourceDir, targetDir, exclusions ??= new string[0], updateCallback);
if (!iterateFilesResult.Succeeded) return iterateDirectoriesResult;
return Result.FromSuccess();
}
catch (Exception ex)
@ -125,33 +139,33 @@ public static class FileHelper
return Result.FromError(ex.Message);
}
}
public static bool StreamAssemblyResourceOut(string resourceName, string outputFilePath)
{
try
{
var assembly = Assembly.GetExecutingAssembly();
FileInfo outputFile = new FileInfo(outputFilePath);
if (outputFile.Exists)
{
outputFile.Delete();
}
if (!outputFile.Directory.Exists)
{
Directory.CreateDirectory(outputFile.Directory.FullName);
}
var resName = assembly.GetManifestResourceNames().First(x => x.EndsWith(resourceName));
using (FileStream fs = File.Create(outputFilePath))
using (Stream s = assembly.GetManifestResourceStream(resName))
{
s.CopyTo(fs);
}
outputFile.Refresh();
return outputFile.Exists;
}
@ -161,11 +175,11 @@ public static class FileHelper
return false;
}
}
public static bool CheckPathForProblemLocations(string path, out PathCheck failedCheck)
{
failedCheck = new();
var problemPaths = new List<PathCheck>()
{
new("Documents", PathCheckType.EndsWith, PathCheckAction.Warn),
@ -180,10 +194,11 @@ public static class FileHelper
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 check in problemPaths)
foreach (var check in problemPaths)
{
switch (check.CheckType)
{
@ -193,6 +208,7 @@ public static class FileHelper
failedCheck = check;
return true;
}
break;
case PathCheckType.Contains:
if (path.ToLower().Contains(check.Target.ToLower()))
@ -200,6 +216,7 @@ public static class FileHelper
failedCheck = check;
return true;
}
break;
case PathCheckType.DriveRoot:
if (Regex.Match(path.ToLower(), @"^\w:(\\|\/)$").Success)
@ -207,11 +224,11 @@ public static class FileHelper
failedCheck = check;
return true;
}
break;
}
}
return false;
}
}

View File

@ -6,7 +6,8 @@ namespace SPTInstaller.Helpers;
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 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))
{
@ -19,16 +20,19 @@ public static class HttpClientProgressExtensions
await download.CopyToAsync(destination);
return;
}
// 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 =>
progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
}
}
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 CopyToAsync(this Stream source, Stream destination, int bufferSize,
IProgress<long> progress = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
@ -40,11 +44,12 @@ public static class HttpClientProgressExtensions
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
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);
totalBytesRead += bytesRead;

View File

@ -7,26 +7,28 @@ namespace SPTInstaller.Helpers;
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()
{
// We can't detect the installed path on non-Windows
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return null;
var uninstallStringValue = Registry.LocalMachine.OpenSubKey(registryInstall, false)
?.GetValue("InstallLocation");
var info = (uninstallStringValue is string key) ? new DirectoryInfo(key) : null;
return info?.FullName;
}
public static Result DetectOriginalGameVersion(string gamePath)
{
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);
}
catch (Exception ex)

View File

@ -25,45 +25,45 @@ public static class ProcessHelper
return Result.FromError(
$"Could not find executable ({executable.Name}) or working directory ({workingDir.Name})");
}
var process = new Process();
process.StartInfo.FileName = executable.FullName;
process.StartInfo.WorkingDirectory = workingDir.FullName;
process.EnableRaisingEvents = true;
process.StartInfo.Arguments = "autoclose";
process.Start();
process.WaitForExit();
switch ((PatcherExitCode)process.ExitCode)
{
case PatcherExitCode.Success:
return Result.FromSuccess("Patcher Finished Successfully, extracting Aki");
case PatcherExitCode.ProgramClosed:
return Result.FromError("Patcher was closed before completing!");
case PatcherExitCode.EftExeNotFound:
return Result.FromError("EscapeFromTarkov.exe is missing from the install Path");
case PatcherExitCode.NoPatchFolder:
return Result.FromError("Patchers Folder called 'Aki_Patches' is missing");
case PatcherExitCode.MissingFile:
return Result.FromError("EFT files was missing a Vital file to continue");
case PatcherExitCode.PatchFailed:
return Result.FromError("A patch failed to apply");
default:
return Result.FromError("an unknown error occurred in the patcher");
}
}
public static ReadProcessResult RunAndReadProcessOutputs(string fileName, string args, int timeout = 5000)
{
using var proc = new Process();
proc.StartInfo = new ProcessStartInfo
{
FileName = fileName,
@ -72,13 +72,13 @@ public static class ProcessHelper
RedirectStandardError = true,
CreateNoWindow = true
};
var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
using AutoResetEvent outputWaitHandle = new AutoResetEvent(false);
using AutoResetEvent errorWaitHandle = new AutoResetEvent(false);
proc.OutputDataReceived += (s, e) =>
{
if (e.Data == null)
@ -90,7 +90,7 @@ public static class ProcessHelper
outputBuilder.AppendLine(e.Data);
}
};
proc.ErrorDataReceived += (s, e) =>
{
if (e.Data == null)
@ -102,7 +102,7 @@ public static class ProcessHelper
errorBuilder.AppendLine(e.Data);
}
};
try
{
proc.Start();
@ -111,10 +111,10 @@ public static class ProcessHelper
{
return ReadProcessResult.FromError(ex.Message);
}
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
if (!proc.WaitForExit(timeout) || !outputWaitHandle.WaitOne(timeout) || !errorWaitHandle.WaitOne(timeout))
{
return ReadProcessResult.FromError("Process timed out");

View File

@ -14,22 +14,22 @@ internal static class ServiceHelper
private static bool TryRegisterInstance<T, T2>(object[] parameters = null)
{
var instance = Activator.CreateInstance(typeof(T2), parameters);
if (instance != null)
{
Locator.CurrentMutable.RegisterConstant<T>((T)instance);
return true;
}
return false;
}
/// <summary>
/// Register a class as a service
/// </summary>
/// <typeparam name="T">class to register</typeparam>
public static void Register<T>() where T : class => Register<T, T>();
/// <summary>
/// Register a class as a service by another type
/// </summary>
@ -38,33 +38,33 @@ internal static class ServiceHelper
public static void Register<T, T2>() where T : class
{
var constructors = typeof(T2).GetConstructors();
foreach(var constructor in constructors)
foreach (var constructor in constructors)
{
var parmesan = constructor.GetParameters();
if(parmesan.Length == 0)
if (parmesan.Length == 0)
{
if (TryRegisterInstance<T, T2>()) return;
continue;
}
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 parmValue = Get(parm.ParameterType);
if (parmValue != null) parameters.Add(parmValue);
}
if (TryRegisterInstance<T, T2>(parameters.ToArray())) return;
}
}
/// <summary>
/// Get a service from splat
/// </summary>
@ -74,17 +74,17 @@ internal static class ServiceHelper
public static object Get(Type type)
{
var service = Locator.Current.GetService(type);
if (service == null)
{
var message = $"Could not locate service of type '{type.Name}'";
Log.Error(message);
throw new InvalidOperationException(message);
}
return service;
}
/// <summary>
/// Get a service from splat
/// </summary>
@ -94,17 +94,17 @@ internal static class ServiceHelper
public static T Get<T>()
{
var service = Locator.Current.GetService<T>();
if (service == null)
{
var message = $"Could not locate service of type '{nameof(T)}'";
Log.Error(message);
throw new InvalidOperationException(message);
}
return service;
}
/// <summary>
/// Get all services of a type
/// </summary>
@ -114,14 +114,14 @@ internal static class ServiceHelper
public static T[] GetAll<T>()
{
var services = Locator.Current.GetServices<T>().ToArray();
if (services == null || services.Count() == 0)
{
var message = $"Could not locate service of type '{nameof(T)}'";
Log.Error(message);
throw new InvalidOperationException(message);
}
return services;
}
}

View File

@ -5,32 +5,30 @@ namespace SPTInstaller.Helpers;
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
{
using var archiveStream = archiveFile.OpenRead();
var dllPath = Path.Join(DownloadCacheHelper.CachePath, "7z.dll");
SevenZipBase.SetLibraryPath(dllPath);
var extractor = new SevenZipExtractor(archiveStream);
extractor.Extracting += (_, args) =>
{
progress.Report(args.PercentDone);
};
extractor.Extracting += (_, args) => { progress.Report(args.PercentDone); };
extractor.ExtractArchive(outputDirectory.FullName);
outputDirectory.Refresh();
if (!outputDirectory.Exists)
{
return Result.FromError($"Failed to extract files: {archiveFile.Name}");
}
return Result.FromSuccess();
}
catch (Exception ex)

View File

@ -8,22 +8,23 @@ namespace SPTInstaller.Installer_Tasks;
public class CopyClientTask : InstallerTaskBase
{
private InternalData _data;
public CopyClientTask(InternalData data) : base("Copy Client Files")
{
_data = data;
}
public override async Task<IResult> TaskOperation()
{
SetStatus("Copying Client Files", "", 0);
var originalGameDirInfo = new DirectoryInfo(_data.OriginalGamePath);
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
// relative path for exclusions
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);
}
}

View File

@ -15,18 +15,18 @@ public class DownloadTask : InstallerTaskBase
private InternalData _data;
private List<IMirrorDownloader> _mirrors = new List<IMirrorDownloader>();
private string _expectedPatcherHash = "";
public DownloadTask(InternalData data) : base("Download Files")
{
_data = data;
}
private async Task<IResult> BuildMirrorList()
{
foreach (var mirror in _data.PatchInfo.Mirrors)
{
_expectedPatcherHash = mirror.Hash;
switch (mirror.Link)
{
case { } l when l.StartsWith("https://mega"):
@ -37,77 +37,79 @@ public class DownloadTask : InstallerTaskBase
break;
}
}
return Result.FromSuccess("Mirrors list ready");
}
private async Task<IResult> DownloadPatcherFromMirrors(IProgress<double> progress)
{
SetStatus("Downloading Patcher", "Verifying cached patcher ...", progressStyle: ProgressStyle.Indeterminate);
if (DownloadCacheHelper.CheckCache("patcher", _expectedPatcherHash, out var 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();
}
foreach (var mirror in _mirrors)
{
SetStatus("Downloading Patcher", mirror.MirrorInfo.Link, progressStyle: ProgressStyle.Indeterminate);
_data.PatcherZipInfo = await mirror.Download(progress);
if (_data.PatcherZipInfo != null)
{
return Result.FromSuccess();
}
}
return Result.FromError("Failed to download Patcher");
}
private async Task<IResult> DownloadSptAkiFromMirrors(IProgress<double> progress)
{
// Note that GetOrDownloadFileAsync handles the cached file hash check, so we don't need to check it first
foreach (var mirror in _data.ReleaseInfo.Mirrors)
{
SetStatus("Downloading SPT-AKI", mirror.DownloadUrl, progressStyle: ProgressStyle.Indeterminate);
_data.AkiZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki", mirror.DownloadUrl, progress, mirror.Hash);
_data.AkiZipInfo =
await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki", mirror.DownloadUrl, progress, mirror.Hash);
if (_data.AkiZipInfo != null)
{
return Result.FromSuccess();
}
}
return Result.FromError("Failed to download spt-aki");
}
public override async Task<IResult> TaskOperation()
{
var progress = new Progress<double>((d) => { SetStatus(null, null, (int)Math.Floor(d)); });
if (_data.PatchNeeded)
{
var buildResult = await BuildMirrorList();
if (!buildResult.Succeeded)
{
return buildResult;
}
SetStatus(null, null, 0);
var patcherDownloadRresult = await DownloadPatcherFromMirrors(progress);
if (!patcherDownloadRresult.Succeeded)
{
return patcherDownloadRresult;
}
}
return await DownloadSptAkiFromMirrors(progress);
}
}

View File

@ -8,37 +8,39 @@ namespace SPTInstaller.Installer_Tasks;
public class InitializationTask : InstallerTaskBase
{
private InternalData _data;
public InitializationTask(InternalData data) : base("Startup")
{
_data = data;
}
public override async Task<IResult> TaskOperation()
{
SetStatus("Initializing", $"Installed EFT Game Path: {FileHelper.GetRedactedPath(_data.OriginalGamePath)}");
var result = PreCheckHelper.DetectOriginalGameVersion(_data.OriginalGamePath);
if (!result.Succeeded)
{
return result;
}
_data.OriginalGameVersion = result.Message;
SetStatus(null, $"Installed EFT Game Version: {_data.OriginalGameVersion}");
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")))
{
return Result.FromError("Installer is located in a folder that has existing game files. Please make sure the installer is in an empty folder as per the guide");
return Result.FromError(
"Installer is located in a folder that has existing game files. Please make sure the installer is in an empty folder as per the guide");
}
return Result.FromSuccess($"Current Game Version: {_data.OriginalGameVersion}");
}
}

View File

@ -10,34 +10,34 @@ public class EftLauncherPreCheck : PreCheckBase
public EftLauncherPreCheck() : base("EFT Launcher Closed", true)
{
}
public async override Task<PreCheckResult> CheckOperation()
{
var eftLauncherProcs = Process.GetProcessesByName("BsgLauncher");
return eftLauncherProcs.Length == 0
? PreCheckResult.FromSuccess("Eft launcher is closed")
: PreCheckResult.FromError("Eft launcher is open. Please close it to install SPT",
"Kill EFT Launcher Processes",
: PreCheckResult.FromError("Eft launcher is open. Please close it to install SPT",
"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();
proc.WaitForExit();
Log.Information($"Killed Proc: {proc.ProcessName}#{proc.Id}");
try
{
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)
{
Log.Error(ex, $"Failed to kill proc: {proc.ProcessName}#{proc.Id}");
}
}
RequestReevaluation();
});
RequestReevaluation();
});
}
}

View File

@ -8,46 +8,85 @@ namespace SPTInstaller.Installer_Tasks.PreChecks;
public class FreeSpacePreCheck : PreCheckBase
{
private readonly InternalData _internalData;
public FreeSpacePreCheck(InternalData internalData) : base("Free Space", true)
{
_internalData = internalData;
}
public override async Task<PreCheckResult> CheckOperation()
{
if (_internalData.OriginalGamePath is null)
return PreCheckResult.FromError("Could not find EFT game path");
if (_internalData.TargetInstallPath is null)
return PreCheckResult.FromError("Could not find install target path");
try
{
var eftSourceDirectoryInfo = new DirectoryInfo(_internalData.OriginalGamePath);
var installTargetDirectoryInfo = new DirectoryInfo(_internalData.TargetInstallPath);
var cacheDirectory = new DirectoryInfo(DownloadCacheHelper.CachePath);
var eftSourceDirSize = DirectorySizeHelper.GetSizeOfDirectory(eftSourceDirectoryInfo);
if (eftSourceDirSize == -1)
{
return PreCheckResult.FromError("An error occurred while getting the EFT source directory size");
}
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
eftSourceDirSize += 10000000000;
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)
{
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)
{

View File

@ -12,14 +12,14 @@ public class Net8PreCheck : PreCheckBase
public Net8PreCheck() : base(".Net 8 Desktop Runtime", true)
{
}
public override async Task<PreCheckResult> CheckOperation()
{
var minRequiredVersion = new Version("8.0.0");
string[] output;
var failedButtonText = "Download .Net 8 Desktop Runtime";
var failedButtonAction = () =>
{
Process.Start(new ProcessStartInfo
@ -27,20 +27,26 @@ public class Net8PreCheck : PreCheckBase
FileName = "cmd.exe",
UseShellExecute = true,
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
{
var programFiles = Environment.ExpandEnvironmentVariables("%ProgramW6432%");
var result = ProcessHelper.RunAndReadProcessOutputs($@"{programFiles}\dotnet\dotnet.exe", "--list-runtimes");
var result =
ProcessHelper.RunAndReadProcessOutputs($@"{programFiles}\dotnet\dotnet.exe", "--list-runtimes");
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");
}
catch (Exception ex)
@ -48,28 +54,31 @@ public class Net8PreCheck : PreCheckBase
Log.Error(ex, $"PreCheck::{Name}::Exception");
return PreCheckResult.FromException(ex);
}
var highestFoundVersion = new Version("0.0.0");
foreach (var lineVersion in output)
{
var regex = Regex.Match(lineVersion, @"Microsoft.WindowsDesktop.App (\d\.\d\.\d)");
if (!regex.Success || regex.Groups.Count < 1)
continue;
var stringVersion = regex.Groups[1].Value;
var foundVersion = new Version(stringVersion);
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;
}
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);
}
}

View File

@ -7,20 +7,21 @@ using SPTInstaller.Helpers;
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")]
[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 NetCore6PreCheck() : base(".Net Core 6 Desktop Runtime", true)
{
}
public override async Task<PreCheckResult> CheckOperation()
{
var minRequiredVersion = new Version("6.0.0");
string[] output;
var failedButtonText = "Download .Net Core 6 Desktop Runtime";
var failedButtonAction = () =>
{
Process.Start(new ProcessStartInfo
@ -28,20 +29,26 @@ public class NetCore6PreCheck : PreCheckBase
FileName = "cmd.exe",
UseShellExecute = true,
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
{
var programFiles = Environment.ExpandEnvironmentVariables("%ProgramW6432%");
var result = ProcessHelper.RunAndReadProcessOutputs($@"{programFiles}\dotnet\dotnet.exe", "--list-runtimes");
var result =
ProcessHelper.RunAndReadProcessOutputs($@"{programFiles}\dotnet\dotnet.exe", "--list-runtimes");
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");
}
catch (Exception ex)
@ -49,28 +56,31 @@ public class NetCore6PreCheck : PreCheckBase
Log.Error(ex, $"PreCheck::{Name}::Exception");
return PreCheckResult.FromException(ex);
}
var highestFoundVersion = new Version("0.0.0");
foreach (var lineVersion in output)
{
var regex = Regex.Match(lineVersion, @"Microsoft.WindowsDesktop.App (\d\.\d\.\d)");
if (!regex.Success || regex.Groups.Count < 1)
continue;
var stringVersion = regex.Groups[1].Value;
var foundVersion = new Version(stringVersion);
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;
}
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);
}
}

View File

@ -11,17 +11,17 @@ public class NetFramework472PreCheck : PreCheckBase
public NetFramework472PreCheck() : base(".Net Framework 4.7.2", true)
{
}
public override async Task<PreCheckResult> CheckOperation()
{
try
{
var minRequiredVersion = new Version("4.7.2");
var key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full");
var failedButtonText = "Download .Net Framework 4.7.2";
var failedButtonAction = () =>
{
Process.Start(new ProcessStartInfo
@ -29,30 +29,40 @@ public class NetFramework472PreCheck : PreCheckBase
FileName = "cmd.exe",
UseShellExecute = true,
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)
{
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");
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);
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)
{

View File

@ -3,20 +3,23 @@ using SPTInstaller.Models;
using System.Threading.Tasks;
namespace SPTInstaller.Installer_Tasks.PreChecks;
public class TestPreCheck : PreCheckBase
{
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)
{
_endState = endState;
}
public override async Task<PreCheckResult> CheckOperation()
{
await Task.Delay(1000);
switch (_endState)
{
case StatusSpinner.SpinnerState.Error:
@ -27,4 +30,4 @@ public class TestPreCheck : PreCheckBase
return PreCheckResult.FromSuccess("This is what a successful precheck looks like");
}
}
}
}

View File

@ -11,40 +11,44 @@ namespace SPTInstaller.Installer_Tasks;
public class ReleaseCheckTask : InstallerTaskBase
{
private InternalData _data;
public ReleaseCheckTask(InternalData data) : base("Release Checks")
{
_data = data;
}
public override async Task<IResult> TaskOperation()
{
try
{
SetStatus("Checking SPT Releases", "", null, ProgressStyle.Indeterminate);
var progress = new Progress<double>((d) => { SetStatus(null, null, (int)Math.Floor(d)); });
var akiReleaseInfoFile = await DownloadCacheHelper.DownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl, progress);
var akiReleaseInfoFile =
await DownloadCacheHelper.DownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress);
if (akiReleaseInfoFile == null)
{
return Result.FromError("Failed to download release metadata");
}
var akiReleaseInfo = JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(akiReleaseInfoFile.FullName));
var akiReleaseInfo =
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(akiReleaseInfoFile.FullName));
SetStatus("Checking for Patches", "", null, ProgressStyle.Indeterminate);
var akiPatchMirrorsFile =
await DownloadCacheHelper.DownloadFileAsync("mirrors.json", DownloadCacheHelper.PatchMirrorUrl,
progress);
if (akiPatchMirrorsFile == null)
{
return Result.FromError("Failed to download patch mirror data");
}
var patchMirrorInfo = JsonConvert.DeserializeObject<PatchInfo>(File.ReadAllText(akiPatchMirrorsFile.FullName));
var patchMirrorInfo =
JsonConvert.DeserializeObject<PatchInfo>(File.ReadAllText(akiPatchMirrorsFile.FullName));
if (akiReleaseInfo == null || patchMirrorInfo == null)
{
return Result.FromError("An error occurred while deserializing aki or patch data");
@ -58,28 +62,31 @@ public class ReleaseCheckTask : InstallerTaskBase
// note: it's possible the game version could be lower than the aki version and still need a patch if the major version numbers change
// : it's probably a low chance though
bool patchNeedCheck = intGameVersion > intAkiVersion;
if (intGameVersion < intAkiVersion)
{
return Result.FromError("Your client is outdated. Please update EFT");
}
if (intGameVersion == intAkiVersion)
{
patchNeedCheck = false;
}
if ((intGameVersion != patchMirrorInfo.SourceClientVersion || intAkiVersion != patchMirrorInfo.TargetClientVersion) && patchNeedCheck)
if ((intGameVersion != patchMirrorInfo.SourceClientVersion ||
intAkiVersion != patchMirrorInfo.TargetClientVersion) && patchNeedCheck)
{
return Result.FromError("No patcher available for your version.\nA patcher is usually created within 24 hours of an EFT update.");
return Result.FromError(
"No patcher available for your version.\nA patcher is usually created within 24 hours of an EFT update.");
}
_data.PatchNeeded = patchNeedCheck;
string status = $"Current Release: {akiReleaseInfo.ClientVersion} - {(_data.PatchNeeded ? "Patch Available" : "No Patch Needed")}";
string status =
$"Current Release: {akiReleaseInfo.ClientVersion} - {(_data.PatchNeeded ? "Patch Available" : "No Patch Needed")}";
SetStatus(null, status);
return Result.FromSuccess(status);
}
catch (Exception ex)

View File

@ -9,83 +9,84 @@ namespace SPTInstaller.Installer_Tasks;
public class SetupClientTask : InstallerTaskBase
{
private InternalData _data;
public SetupClientTask(InternalData data) : base("Setup Client")
{
_data = data;
}
public override async Task<IResult> TaskOperation()
{
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
var patcherOutputDir = new DirectoryInfo(Path.Join(_data.TargetInstallPath, "patcher"));
var patcherEXE = new FileInfo(Path.Join(_data.TargetInstallPath, "patcher.exe"));
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)
{
// extract patcher files
SetStatus("Extrating Patcher", "", 0);
var extractPatcherResult = ZipHelper.Decompress(_data.PatcherZipInfo, patcherOutputDir, progress);
if (!extractPatcherResult.Succeeded)
{
return extractPatcherResult;
}
// copy patcher files to install directory
SetStatus("Copying Patcher", "", 0);
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)
{
return copyPatcherResult;
}
// run patcher
SetStatus("Running Patcher", "", null, ProgressStyle.Indeterminate);
var patchingResult = ProcessHelper.PatchClientFiles(patcherEXE, targetInstallDirInfo);
if (!patchingResult.Succeeded)
{
return patchingResult;
}
}
// extract release files
SetStatus("Extracting Release", "", 0);
var extractReleaseResult = ZipHelper.Decompress(_data.AkiZipInfo, targetInstallDirInfo, progress);
if (!extractReleaseResult.Succeeded)
{
return extractReleaseResult;
}
// cleanup temp files
SetStatus("Cleanup", "almost done :)", null, ProgressStyle.Indeterminate);
if(_data.PatchNeeded)
if (_data.PatchNeeded)
{
patcherOutputDir.Delete(true);
patcherEXE.Delete();
}
return Result.FromSuccess("SPT is Setup. Happy Playing!");
}
}

View File

@ -7,27 +7,27 @@ namespace SPTInstaller.Installer_Tasks;
internal class TestTask : InstallerTaskBase
{
public static TestTask FromRandomName() => new TestTask($"Test Task #{new Random().Next(0, 9999)}");
public TestTask(string name) : base(name)
{
}
public async override Task<IResult> TaskOperation()
{
var total = 4;
var interval = TimeSpan.FromSeconds(1);
for(var i = 0; i < total; i++)
for (var i = 0; i < total; i++)
{
var count = i + 1;
var progressMessage = $"Running Task: {Name}";
var progress = (int)Math.Floor((double)count / total * 100);
SetStatus(progressMessage, $"Details: ({count}/{total})", progress);
await Task.Delay(interval);
}
return Result.FromSuccess();
}
}

View File

@ -2,8 +2,9 @@
using System.Threading.Tasks;
namespace SPTInstaller.Interfaces;
public interface IMirrorDownloader
{
public PatchInfoMirror MirrorInfo { get; }
public Task<FileInfo?> Download(IProgress<double> progress);
}
}

View File

@ -11,6 +11,6 @@ public interface IPreCheck
public string PreCheckDetails { get; }
public StatusSpinner.SpinnerState State { get; set; }
public event EventHandler ReeevaluationRequested;
public Task<IResult> RunCheck();
}

View File

@ -6,18 +6,18 @@ public interface IProgressableTask
{
public string Id { get; }
public string Name { get; }
public bool IsCompleted { get; }
public bool HasErrors { get; }
public bool IsRunning { get; }
public string StatusMessage { get; }
public int Progress { get; }
public bool ShowProgress { get; }
public Task<IResult> RunAsync();
}

View 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; }
}

View File

@ -8,82 +8,92 @@ namespace SPTInstaller.Models;
public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
{
private string _id;
public string Id
{
get => _id;
private set => this.RaiseAndSetIfChanged(ref _id, value);
}
private string _name;
public string Name
{
get => _name;
private set => this.RaiseAndSetIfChanged(ref _name, value);
}
private bool _isComleted;
public bool IsCompleted
{
get => _isComleted;
private set => this.RaiseAndSetIfChanged(ref _isComleted, value);
}
private bool _hasErrors;
public bool HasErrors
{
get => _hasErrors;
private set => this.RaiseAndSetIfChanged(ref _hasErrors, value);
}
private bool _isRunning;
public bool IsRunning
{
get => _isRunning;
private set => this.RaiseAndSetIfChanged(ref _isRunning, value);
}
private int _progress;
public int Progress
{
get => _progress;
private set => this.RaiseAndSetIfChanged(ref _progress, value);
}
private bool _showProgress;
public bool ShowProgress
{
get => _showProgress;
private set => this.RaiseAndSetIfChanged(ref _showProgress, value);
}
private bool _indeterminateProgress;
public bool IndeterminateProgress
{
get => _indeterminateProgress;
private set => this.RaiseAndSetIfChanged(ref _indeterminateProgress, value);
}
private string _statusMessage;
public string StatusMessage
{
get => _statusMessage;
private set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
private string _statusDetails;
public string StatusDetails
{
get => _statusDetails;
private set => this.RaiseAndSetIfChanged(ref _statusDetails, value);
}
public enum ProgressStyle
{
Hidden = 0,
Shown,
Indeterminate,
}
/// <summary>
/// Update the status details of the task
/// </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="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>
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))
{
Log.Information($" <===> {message} <===>");
}
StatusMessage = message;
}
if(details != null && details != StatusDetails)
if (details != null && details != StatusDetails)
{
if (!noLog && !string.IsNullOrWhiteSpace(details))
{
Log.Information(details);
}
StatusDetails = details;
}
if (progressStyle != null)
{
switch (progressStyle)
@ -131,7 +142,7 @@ public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
break;
}
}
if (progress != null)
{
ShowProgress = true;
@ -139,13 +150,13 @@ public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
Progress = progress.Value;
}
}
public InstallerTaskBase(string name)
{
Name = name;
Id = Guid.NewGuid().ToString();
}
/// <summary>
/// A method for the install controller to call. Do not use this within your task
/// </summary>
@ -153,23 +164,23 @@ public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
public async Task<IResult> RunAsync()
{
IsRunning = true;
var result = await TaskOperation();
IsRunning = false;
if (!result.Succeeded)
{
HasErrors = true;
return result;
}
IsCompleted = true;
return result;
}
/// <summary>
/// The task you want to run
/// </summary>

View File

@ -3,105 +3,118 @@ using Serilog;
using SPTInstaller.Helpers;
using System.Diagnostics;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace SPTInstaller.Models;
public class InstallerUpdateInfo : ReactiveObject
{
private Version? _newVersion;
public string NewInstallerUrl = "";
public Version? NewVersion { get; private set; }
public string ChangeLog = "";
private string _updateInfoText = "";
public string UpdateInfoText
{
get => _updateInfoText;
set => this.RaiseAndSetIfChanged(ref _updateInfoText, value);
}
private bool _show = false;
public bool Show
{
get => _show;
set => this.RaiseAndSetIfChanged(ref _show, value);
}
private bool _updating = false;
public bool Updating
{
get => _updating;
set => this.RaiseAndSetIfChanged(ref _updating, value);
}
private bool _updateAvailable = false;
public bool UpdateAvailable
{
get => _updateAvailable;
set => this.RaiseAndSetIfChanged(ref _updateAvailable, value);
}
private bool _checkingForUpdates = false;
public bool CheckingForUpdates
{
get => _checkingForUpdates;
set => this.RaiseAndSetIfChanged(ref _checkingForUpdates, value);
}
private int _downloadProgress;
public int DownloadProgress
{
get => _downloadProgress;
set => this.RaiseAndSetIfChanged(ref _downloadProgress, value);
}
public async Task UpdateInstaller()
{
Updating = true;
UpdateAvailable = false;
var updater = new FileInfo(Path.Join(DownloadCacheHelper.CachePath, "update.ps1"));
if (!FileHelper.StreamAssemblyResourceOut("update.ps1", updater.FullName))
{
Log.Fatal("Failed to prepare update file");
return;
}
if (!updater.Exists)
{
UpdateInfoText = "Failed to get updater from resources :(";
return;
}
var newInstallerPath = await DownloadNewInstaller();
if(string.IsNullOrWhiteSpace(newInstallerPath))
if (string.IsNullOrWhiteSpace(newInstallerPath))
return;
Process.Start(new ProcessStartInfo
{
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()
{
UpdateInfoText = $"Downloading installer v{_newVersion}";
UpdateInfoText = $"Downloading installer v{NewVersion}";
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)
{
UpdateInfoText = "Failed to download new installer :(";
return "";
}
return file.FullName;
}
private void EndCheck(string infoText, bool updateAvailable, bool log = true)
{
if (log)
@ -114,58 +127,62 @@ public class InstallerUpdateInfo : ReactiveObject
CheckingForUpdates = false;
UpdateAvailable = updateAvailable;
}
// public async Task CheckForUpdates(Version? currentVersion)
// {
// if (currentVersion == null)
// return;
//
// UpdateInfoText = "Checking for installer updates";
// Show = true;
// CheckingForUpdates = true;
//
// try
// {
// var repo = new RepositoryApi(Configuration.Default);
//
// var releases = await repo.RepoListReleasesAsync("CWX", "SPT-AKI-Installer");
//
// if (releases == null || releases.Count == 0)
// {
// EndCheck("No releases available", false);
// return;
// }
//
// var latest = releases.FindAll(x => !x.Prerelease)[0];
//
// if (latest == null)
// {
// EndCheck("could not get latest release", false);
// return;
// }
//
// var latestVersion = new Version(latest.TagName);
//
// if (latestVersion == null || latestVersion <= currentVersion)
// {
// EndCheck("No updates available", false);
// return;
// }
//
// _newVersion = latestVersion;
//
// NewInstallerUrl = latest.Assets[0].BrowserDownloadUrl;
//
// EndCheck($"Update available: v{latestVersion}", true);
//
// return;
// }
// catch (Exception ex)
// {
// EndCheck(ex.Message, false, false);
// Log.Error(ex, "Failed to check for updates");
// }
//
// return;
// }
}
public async Task CheckForUpdates(Version? currentVersion)
{
if (currentVersion == null)
return;
UpdateInfoText = "Checking for installer updates";
Show = true;
CheckingForUpdates = true;
try
{
var installerInfoFile =
await DownloadCacheHelper.DownloadFileAsync("installer.json", DownloadCacheHelper.InstallerInfoUrl,
null);
if (installerInfoFile == null)
{
EndCheck("Failed to download installer info", false);
return;
}
var installerInfo =
JsonConvert.DeserializeObject<InstallerInfo>(File.ReadAllText(installerInfoFile.FullName));
if (installerInfo == null)
{
EndCheck("Failed to parse installer info json", false);
return;
}
var latestVersion = new Version(installerInfo.LatestVersion);
if (latestVersion <= currentVersion)
{
EndCheck("No updates available", false);
return;
}
NewVersion = latestVersion;
foreach (var change in installerInfo.Changes)
{
ChangeLog += $"◉ {change}\n";
}
EndCheck($"Update Installer: v{latestVersion}", true);
return;
}
catch (Exception ex)
{
EndCheck(ex.Message, false, false);
Log.Error(ex, "Failed to check for updates");
}
return;
}
}

View File

@ -9,39 +9,39 @@ public class InternalData
/// The folder to install SPT into
/// </summary>
public string? TargetInstallPath { get; set; }
/// <summary>
/// The orginal EFT game path
/// </summary>
public string? OriginalGamePath { get; set; }
/// <summary>
/// The original EFT game version
/// </summary>
public string OriginalGameVersion { get; set; }
/// <summary>
/// Patcher zip file info
/// </summary>
public FileInfo PatcherZipInfo { get; set; }
/// <summary>
/// SPT-AKI zip file info
/// </summary>
public FileInfo AkiZipInfo { get; set; }
/// <summary>
/// The release information from release.json
/// </summary>
public ReleaseInfo.ReleaseInfo ReleaseInfo { get; set; }
public PatchInfo PatchInfo { get; set; }
/// <summary>
/// The release download link for the patcher mirror list
/// </summary>
// public string PatcherMirrorsLink { get; set; }
/// <summary>
/// Whether or not a patch is needed to downgrade the client files
/// </summary>

View File

@ -2,19 +2,20 @@
using System.Threading.Tasks;
namespace SPTInstaller.Models.Mirrors.Downloaders;
public class HttpMirrorDownloader : MirrorDownloaderBase
{
public HttpMirrorDownloader(PatchInfoMirror mirror) : base(mirror)
{
}
public override async Task<FileInfo?> Download(IProgress<double> progress)
{
var file = await DownloadCacheHelper.DownloadFileAsync("patcher", MirrorInfo.Link, progress);
if (file == null)
return null;
return FileHashHelper.CheckHash(file, MirrorInfo.Hash) ? file : null;
}
}
}

View File

@ -4,26 +4,27 @@ using System.Threading.Tasks;
using Serilog;
namespace SPTInstaller.Models.Mirrors.Downloaders;
public class MegaMirrorDownloader : MirrorDownloaderBase
{
public MegaMirrorDownloader(PatchInfoMirror mirrorInfo) : base(mirrorInfo)
{
}
public override async Task<FileInfo?> Download(IProgress<double> progress)
{
var megaClient = new MegaApiClient();
await megaClient.LoginAnonymousAsync();
// if mega fails to connect, just return
if (!megaClient.IsLoggedIn)
return null;
try
{
var file = new FileInfo(Path.Join(DownloadCacheHelper.CachePath, "patcher"));
if (file.Exists)
if (file.Exists)
{
file.Delete();
}
@ -32,17 +33,17 @@ public class MegaMirrorDownloader : MirrorDownloaderBase
file.FullName, progress);
file.Refresh();
if (!file.Exists)
return null;
return FileHashHelper.CheckHash(file, MirrorInfo.Hash) ? file : null;
}
catch(Exception ex)
catch (Exception ex)
{
Log.Error(ex, "Exception thrown while downloading from mega");
//most likely a 509 (Bandwidth limit exceeded) due to mega's user quotas.
return null;
}
}
}
}

View File

@ -2,12 +2,14 @@
using System.Threading.Tasks;
namespace SPTInstaller.Models.Mirrors.Downloaders;
public abstract class MirrorDownloaderBase : IMirrorDownloader
{
public PatchInfoMirror MirrorInfo { get; private set; }
public abstract Task<FileInfo?> Download(IProgress<double> progress);
public MirrorDownloaderBase(PatchInfoMirror mirrorInfo)
{
MirrorInfo = mirrorInfo;
}
}
}

View File

@ -18,7 +18,7 @@ public class PathCheck
public string Target { get; private set; }
public PathCheckType CheckType { get; private set; }
public PathCheckAction CheckAction { get; private set; }
public PathCheck()
{
}

View File

@ -8,70 +8,79 @@ namespace SPTInstaller.Models;
public abstract class PreCheckBase : ReactiveObject, IPreCheck
{
public event EventHandler ReeevaluationRequested = delegate { };
public event EventHandler ReeevaluationRequested = delegate { };
private string _id;
public string Id
{
get => _id;
set => this.RaiseAndSetIfChanged(ref _id, value);
}
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
}
private string _name;
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
private bool _required;
public bool IsRequired
{
get => _required;
set => this.RaiseAndSetIfChanged(ref _required, value);
}
private StatusSpinner.SpinnerState _state;
public StatusSpinner.SpinnerState State
{
get => _state;
set => this.RaiseAndSetIfChanged(ref _state, value);
}
private string _preCheckDetails;
public string PreCheckDetails
{
get => _preCheckDetails;
set => this.RaiseAndSetIfChanged(ref _preCheckDetails, value);
}
private bool _actionButtonIsVisible;
public bool ActionButtonIsVisible
{
get => _actionButtonIsVisible;
set => this.RaiseAndSetIfChanged(ref _actionButtonIsVisible, 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);
}
/// <summary>
/// Base class for pre-checks to run before installation
/// </summary>
@ -83,15 +92,15 @@ public abstract class PreCheckBase : ReactiveObject, IPreCheck
IsRequired = required;
Id = Guid.NewGuid().ToString();
}
private StatusSpinner.SpinnerState ProcessResult(PreCheckResult result) =>
(result.Succeeded, IsRequired) switch
{
(true, _) => StatusSpinner.SpinnerState.OK,
(true, _) => StatusSpinner.SpinnerState.OK,
(false, false) => StatusSpinner.SpinnerState.Warning,
(_, _) => StatusSpinner.SpinnerState.Error
(_, _) => StatusSpinner.SpinnerState.Error
};
/// <summary>
/// Request pre-checks to be re-evaluated
/// </summary>
@ -99,25 +108,29 @@ public abstract class PreCheckBase : ReactiveObject, IPreCheck
{
ReeevaluationRequested?.Invoke(this, null);
}
public async Task<IResult> RunCheck()
{
State = StatusSpinner.SpinnerState.Running;
var result = await CheckOperation();
PreCheckDetails = !string.IsNullOrWhiteSpace(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;
ActionButtonCommand = result.ButtonPressedCommand;
ActionButtonIsVisible = result.ActionButtonIsVisible;
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();
}

View File

@ -15,42 +15,47 @@ public class PreCheckDetailInfo : ReactiveObject
}
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;

View File

@ -3,35 +3,42 @@ using SPTInstaller.Interfaces;
using System.Windows.Input;
namespace SPTInstaller.Models;
public class PreCheckResult : IResult
{
public bool Succeeded { get; private set; }
public string Message { get; private set; }
public bool ActionButtonIsVisible { get; private set; }
public string ActionButtonText { get; private set; }
public ICommand ButtonPressedCommand { get; private set; }
protected PreCheckResult(string message, bool succeeded, string actionButtonText, Action? buttonPressedAction)
{
Message = message;
Succeeded = succeeded;
ActionButtonText = actionButtonText;
ActionButtonIsVisible = buttonPressedAction != null && !string.IsNullOrWhiteSpace(actionButtonText);
buttonPressedAction ??= () => { };
ButtonPressedCommand = ReactiveCommand.Create(buttonPressedAction);
}
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 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);
}
public static PreCheckResult FromError(string message, string actionButtonText = "",
Action? actionButtonPressedAction = null) =>
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);
}

View File

@ -5,14 +5,15 @@ public class ReadProcessResult : Result
public string StdOut { 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;
StdErr = stdErr;
}
public static ReadProcessResult FromSuccess(string stdOut, string stdErr) =>
new ReadProcessResult("ok", true, stdOut, stdErr);
public new static ReadProcessResult FromError(string message) => new ReadProcessResult(message, false);
}

View File

@ -1,10 +1,10 @@
using System.Collections.Generic;
namespace SPTInstaller.Models.ReleaseInfo;
public class ReleaseInfo
{
public string AkiVersion { get; set; }
public string ClientVersion { get; set; }
public List<ReleaseInfoMirror> Mirrors { get; set; }
}
}

View File

@ -1,6 +1,7 @@
namespace SPTInstaller.Models.ReleaseInfo;
public class ReleaseInfoMirror
{
public string DownloadUrl { get; set; }
public string Hash { get; set; }
}
}

View File

@ -5,15 +5,15 @@ namespace SPTInstaller.Models;
public class Result : IResult
{
public bool Succeeded { get; private set; }
public string Message { get; private set; }
protected Result(string message, bool succeeded)
{
Message = message;
Succeeded = succeeded;
}
public static Result FromSuccess(string message = "") => new(message, true);
public static Result FromError(string message) => new(message, false);
}

View File

@ -33,22 +33,22 @@ internal class Program
Log.Fatal(ex, "Installer closed unexpectedly");
}
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly());
// Register all the things
// Regestering as base classes so ReactiveUI works correctly. Doesn't seem to like the interfaces :(
ServiceHelper.Register<InternalData>();
#if !TEST
ServiceHelper.Register<PreCheckBase, NetFramework472PreCheck>();
ServiceHelper.Register<PreCheckBase, Net8PreCheck>();
ServiceHelper.Register<PreCheckBase, FreeSpacePreCheck>();
ServiceHelper.Register<PreCheckBase, EftLauncherPreCheck>();
ServiceHelper.Register<InstallerTaskBase, InitializationTask>();
ServiceHelper.Register<InstallerTaskBase, ReleaseCheckTask>();
ServiceHelper.Register<InstallerTaskBase, DownloadTask>();
@ -64,16 +64,16 @@ internal class Program
Locator.CurrentMutable.RegisterConstant<PreCheckBase>(TestPreCheck.FromRandomName(StatusSpinner.SpinnerState.Warning));
Locator.CurrentMutable.RegisterConstant<PreCheckBase>(TestPreCheck.FromRandomName(StatusSpinner.SpinnerState.Error));
#endif
// need the interfaces for the controller and splat won't resolve them since we need to base classes in avalonia (what a mess), so doing it manually here
var tasks = Locator.Current.GetServices<InstallerTaskBase>().ToArray() as IProgressableTask[];
var preChecks = Locator.Current.GetServices<PreCheckBase>().ToArray() as IPreCheck[];
var installer = new InstallController(tasks, preChecks);
// manually register install controller
Locator.CurrentMutable.RegisterConstant(installer);
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()

Binary file not shown.

View File

@ -9,7 +9,8 @@ Write-Host "Stopping installer ..."
$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'"
return;
}

View File

@ -1,5 +1,5 @@
<linker>
<!-- Can be removed if CompiledBinding and no reflection are used -->
<assembly fullname="SPTInstaller" preserve="All" />
<assembly fullname="Avalonia.Themes.Fluent" preserve="All" />
<!-- Can be removed if CompiledBinding and no reflection are used -->
<assembly fullname="SPTInstaller" preserve="All"/>
<assembly fullname="Avalonia.Themes.Fluent" preserve="All"/>
</linker>

View File

@ -1,50 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<PackageIcon>icon.ico</PackageIcon>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<Configurations>Debug;Release;TEST</Configurations>
<AssemblyVersion>2.61</AssemblyVersion>
<FileVersion>2.61</FileVersion>
<Company>SPT-AKI</Company>
</PropertyGroup>
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<PackageIcon>icon.ico</PackageIcon>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<Configurations>Debug;Release;TEST</Configurations>
<AssemblyVersion>2.62</AssemblyVersion>
<FileVersion>2.62</FileVersion>
<Company>SPT-AKI</Company>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
<None Remove="Assets\icon.ico" />
<None Remove="Resources\update.ps1" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**"/>
<None Remove=".gitignore"/>
<None Remove="Assets\icon.ico"/>
<None Remove="Resources\update.ps1"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\update.ps1" />
<None Remove="Resources\7z.dll" />
<EmbeddedResource Include="Resources\7z.dll" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\update.ps1"/>
<None Remove="Resources\7z.dll"/>
<EmbeddedResource Include="Resources\7z.dll"/>
</ItemGroup>
<ItemGroup>
<TrimmerRootDescriptor Include="Roots.xml" />
</ItemGroup>
<ItemGroup>
<TrimmerRootDescriptor Include="Roots.xml"/>
</ItemGroup>
<ItemGroup>
<!--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.Desktop" Version="11.0.5" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.5" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5" />
<PackageReference Include="DialogHost.Avalonia" Version="0.7.7" />
<PackageReference Include="FubarCoder.RestSharp.Portable.HttpClient" Version="4.0.8" />
<PackageReference Include="MegaApiClient" Version="1.10.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SerilogTraceListener" Version="3.2.0" />
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.1.23" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<!--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.Desktop" Version="11.0.5"/>
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.5"/>
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.5"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5"/>
<PackageReference Include="DialogHost.Avalonia" Version="0.7.7"/>
<PackageReference Include="FubarCoder.RestSharp.Portable.HttpClient" Version="4.0.8"/>
<PackageReference Include="MegaApiClient" Version="1.10.4"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/>
<PackageReference Include="SerilogTraceListener" Version="3.2.0"/>
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.1.23"/>
<PackageReference Include="System.Reactive" Version="6.0.0"/>
</ItemGroup>
</Project>

View File

@ -10,15 +10,15 @@ public class ViewLocator : IDataTemplate
{
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object data)
{
return data is ViewModelBase;

View File

@ -11,24 +11,26 @@ namespace SPTInstaller.ViewModels;
public class InstallViewModel : ViewModelBase
{
private IProgressableTask _currentTask;
public IProgressableTask CurrentTask
{
get => _currentTask;
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)
{
var installer = ServiceHelper.Get<InstallController>();
installer.TaskChanged += (sender, task) => CurrentTask = task;
Task.Run(async () =>
Task.Run(async () =>
{
var result = await installer.RunTasks();
NavigateTo(new MessageViewModel(HostScreen, result));
});
}

View File

@ -10,39 +10,43 @@ public class MainWindowViewModel : ReactiveObject, IActivatableViewModel, IScree
{
public RoutingState Router { get; } = new();
public ViewModelActivator Activator { get; } = new();
private string _title;
public string Title
{
get => _title;
set => this.RaiseAndSetIfChanged(ref _title, value);
}
public MainWindowViewModel(bool debugging)
{
Title = $"{(debugging ? "-debug-" : "")} SPT Installer {"v" + Assembly.GetExecutingAssembly().GetName()?.Version?.ToString() ?? "--unknown version--"}";
Title =
$"{(debugging ? "-debug-" : "")} SPT Installer {"v" + Assembly.GetExecutingAssembly().GetName()?.Version?.ToString() ?? "--unknown version--"}";
Log.Information($"========= {Title} Started =========");
Log.Information(Environment.OSVersion.VersionString);
var uiCulture= CultureInfo.InstalledUICulture;
var uiCulture = CultureInfo.InstalledUICulture;
Log.Information("System Language: {iso} - {name}", uiCulture.TwoLetterISOLanguageName, uiCulture.DisplayName);
Router.Navigate.Execute(new PreChecksViewModel(this, debugging));
}
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();
}
}
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;
}

View File

@ -12,70 +12,76 @@ namespace SPTInstaller.ViewModels;
public class MessageViewModel : ViewModelBase
{
private bool _HasErrors;
public bool HasErrors
{
get => _HasErrors;
set => this.RaiseAndSetIfChanged(ref _HasErrors, value);
}
private string _Message;
public string Message
{
get => _Message;
set => this.RaiseAndSetIfChanged(ref _Message, value);
}
private bool _showCloseButton;
public bool ShowCloseButton
{
get => _showCloseButton;
set => this.RaiseAndSetIfChanged(ref _showCloseButton, value);
}
private string _cacheInfoText;
public string CacheInfoText
{
get => _cacheInfoText;
set => this.RaiseAndSetIfChanged(ref _cacheInfoText, value);
}
private StatusSpinner.SpinnerState _cacheCheckState;
public StatusSpinner.SpinnerState CacheCheckState
{
get => _cacheCheckState;
set => this.RaiseAndSetIfChanged(ref _cacheCheckState, value);
}
public ICommand CloseCommand { get; set; } = ReactiveCommand.Create(() =>
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
if (Application.Current.ApplicationLifetime is
Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.Close();
}
});
public MessageViewModel(IScreen Host, IResult result, bool showCloseButton = true, bool noLog = false) : base(Host)
{
ShowCloseButton = showCloseButton;
Message = result.Message;
Task.Run(() =>
{
CacheInfoText = "Getting cache size ...";
CacheCheckState = StatusSpinner.SpinnerState.Running;
CacheInfoText = $"Cache Size: {DownloadCacheHelper.GetCacheSizeText()}";
CacheCheckState = StatusSpinner.SpinnerState.OK;
});
if (result.Succeeded)
{
Log.Information(Message);
return;
}
HasErrors = true;
if (!noLog)
Log.Error(Message);
}

View File

@ -20,7 +20,7 @@ namespace SPTInstaller.ViewModels;
public class PreChecksViewModel : ViewModelBase
{
private bool _hasPreCheckSelected;
public bool HasPreCheckSelected
{
get => _hasPreCheckSelected;
@ -31,32 +31,35 @@ public class PreChecksViewModel : ViewModelBase
public ICommand SelectPreCheckCommand { get; set; }
public ICommand StartInstallCommand { get; set; }
public ICommand UpdateInstallerCommand { get; set; }
public ICommand DismissUpdateCommand { get; set; }
public ICommand WhatsNewCommand { get; set; }
public ICommand LaunchWithDebug { get; set; }
public InstallerUpdateInfo UpdateInfo { get; set; } = new();
private bool _debugging;
public bool Debugging
{
get => _debugging;
set => this.RaiseAndSetIfChanged(ref _debugging, value);
}
private string _installPath;
public string InstallPath
{
get => _installPath;
set => this.RaiseAndSetIfChanged(ref _installPath, value);
}
private string _installButtonText;
public string InstallButtonText
{
get => _installButtonText;
@ -64,27 +67,31 @@ public class PreChecksViewModel : ViewModelBase
}
private bool _allowInstall;
public bool AllowInstall
{
get => _allowInstall;
set => this.RaiseAndSetIfChanged(ref _allowInstall, value);
}
private bool _allowDetailsButton = false;
public bool AllowDetailsButton
{
get => _allowDetailsButton;
set => this.RaiseAndSetIfChanged(ref _allowDetailsButton, value);
}
private string _cacheInfoText;
public string CacheInfoText
{
get => _cacheInfoText;
set => this.RaiseAndSetIfChanged(ref _cacheInfoText, value);
}
private StatusSpinner.SpinnerState _cacheCheckState;
public StatusSpinner.SpinnerState CacheCheckState
{
get => _cacheCheckState;
@ -92,12 +99,13 @@ public class PreChecksViewModel : ViewModelBase
}
private StatusSpinner.SpinnerState _installButtonCheckState;
public StatusSpinner.SpinnerState InstallButtonCheckState
{
get => _installButtonCheckState;
set => this.RaiseAndSetIfChanged(ref _installButtonCheckState, value);
}
private void ReCheckRequested(object? sender, EventArgs e)
{
Task.Run(async () =>
@ -109,45 +117,47 @@ public class PreChecksViewModel : ViewModelBase
}
});
}
public PreChecksViewModel(IScreen host, bool debugging) : base(host)
{
Debugging = debugging;
var data = ServiceHelper.Get<InternalData?>();
var installer = ServiceHelper.Get<InstallController?>();
installer.RecheckRequested += ReCheckRequested;
InstallButtonText = "Please wait ...";
InstallButtonCheckState = StatusSpinner.SpinnerState.Pending;
if (data == null || installer == null)
{
NavigateTo(new MessageViewModel(HostScreen, Result.FromError("Failed to get required service for prechecks")));
NavigateTo(new MessageViewModel(HostScreen,
Result.FromError("Failed to get required service for prechecks")));
return;
}
data.OriginalGamePath = PreCheckHelper.DetectOriginalGamePath();
data.TargetInstallPath = Environment.CurrentDirectory;
InstallPath = data.TargetInstallPath;
Log.Information($"Install Path: {FileHelper.GetRedactedPath(InstallPath)}");
#if !TEST
if (data.OriginalGamePath == null)
{
NavigateTo(new MessageViewModel(HostScreen, Result.FromError("Could not find EFT install.\n\nDo you own and have the game installed?")));
NavigateTo(new MessageViewModel(HostScreen,
Result.FromError("Could not find EFT install.\n\nDo you own and have the game installed?")));
return;
}
#endif
if (data.OriginalGamePath == data.TargetInstallPath)
{
Log.CloseAndFlush();
var logFiles = Directory.GetFiles(InstallPath, "spt-aki-installer_*.log");
// remove log file from original game path if they exist
foreach (var file in logFiles)
{
@ -155,52 +165,59 @@ public class PreChecksViewModel : ViewModelBase
{
File.Delete(file);
}
catch { }
catch
{
}
}
NavigateTo(new MessageViewModel(HostScreen, Result.FromError("Installer is located in EFT's original directory. Please move the installer to a seperate folder as per the guide"), noLog: true));
NavigateTo(new MessageViewModel(HostScreen,
Result.FromError(
"Installer is located in EFT's original directory. Please move the installer to a seperate folder as per the guide"),
noLog: true));
return;
}
Task.Run(async () =>
{
if (FileHelper.CheckPathForProblemLocations(InstallPath, out var failedCheck ))
if (FileHelper.CheckPathForProblemLocations(InstallPath, out var failedCheck))
{
switch (failedCheck.CheckAction)
switch (failedCheck.CheckAction)
{
case PathCheckAction.Warn:
{
case PathCheckAction.Warn:
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
Log.Warning("Problem path detected, confirming install path ...");
var confirmation = await DialogHost.Show(new ConfirmationDialog(
$"We suspect you may be installing into a problematic folder: {failedCheck.Target}.\nYou might want to consider installing somewhere else to avoid issues.\n\nAre you sure you want to install to this path?\n{InstallPath}"));
if (confirmation == null || !bool.TryParse(confirmation.ToString(), out var confirm) ||
!confirm)
{
Log.Information("User declined install path, exiting");
Environment.Exit(0);
}
});
Log.Warning("Problem path detected, confirming install path ...");
var confirmation = await DialogHost.Show(new ConfirmationDialog(
$"We suspect you may be installing into a problematic folder: {failedCheck.Target}.\nYou might want to consider installing somewhere else to avoid issues.\n\nAre you sure you want to install to this path?\n{InstallPath}"));
break;
}
if (confirmation == null || !bool.TryParse(confirmation.ToString(), out var confirm) ||
!confirm)
{
Log.Information("User declined install path, exiting");
Environment.Exit(0);
}
});
case PathCheckAction.Deny:
{
Log.Error("Problem path detected, install denied");
NavigateTo(new MessageViewModel(HostScreen, Result.FromError($"We suspect you may be installing into a problematic folder: {failedCheck.Target}.\nWe won't be letting you install here. Please move the installer to another folder.\nSuggestion: a folder under your drive root, such as 'C:\\spt\\'\nDenied Path: {InstallPath}")));
break;
}
default:
throw new ArgumentOutOfRangeException();
break;
}
case PathCheckAction.Deny:
{
Log.Error("Problem path detected, install denied");
NavigateTo(new MessageViewModel(HostScreen,
Result.FromError(
$"We suspect you may be installing into a problematic folder: {failedCheck.Target}.\nWe won't be letting you install here. Please move the installer to another folder.\nSuggestion: a folder under your drive root, such as 'C:\\spt\\'\nDenied Path: {InstallPath}")));
break;
}
default:
throw new ArgumentOutOfRangeException();
}
Log.Information("User accepted install path");
}
});
LaunchWithDebug = ReactiveCommand.Create(async () =>
{
try
@ -211,7 +228,7 @@ public class PreChecksViewModel : ViewModelBase
FileName = installerPath,
Arguments = "debug"
});
Environment.Exit(0);
}
catch (Exception ex)
@ -219,84 +236,87 @@ public class PreChecksViewModel : ViewModelBase
Log.Error(ex, "Failed to enter debug mode");
}
});
SelectPreCheckCommand = ReactiveCommand.Create(async(PreCheckBase check) =>
SelectPreCheckCommand = ReactiveCommand.Create(async (PreCheckBase check) =>
{
foreach (var precheck in PreChecks)
{
if (check.Id == precheck.Id)
{
precheck.IsSelected = true;
HasPreCheckSelected = true;
continue;
}
precheck.IsSelected = false;
}
});
StartInstallCommand = ReactiveCommand.Create(async () =>
{
UpdateInfo.Show = false;
NavigateTo(new InstallViewModel(HostScreen));
});
UpdateInstallerCommand = ReactiveCommand.Create(async () =>
{
AllowDetailsButton = false;
AllowInstall = false;
await UpdateInfo.UpdateInstaller();
});
DismissUpdateCommand = ReactiveCommand.Create(() =>
{
UpdateInfo.Show = false;
});
DismissUpdateCommand = ReactiveCommand.Create(() => { UpdateInfo.Show = false; });
WhatsNewCommand =
ReactiveCommand.Create(async () => await DialogHost.Show(new ChangeLogDialog(UpdateInfo.NewVersion.ToString(), UpdateInfo.ChangeLog)));
Task.Run(async () =>
{
// run prechecks
var result = await installer.RunPreChecks();
// check for updates
//await UpdateInfo.CheckForUpdates(Assembly.GetExecutingAssembly().GetName()?.Version);
await UpdateInfo.CheckForUpdates(Assembly.GetExecutingAssembly().GetName()?.Version);
// get latest spt version
InstallButtonText = "Getting latest release ...";
InstallButtonCheckState = StatusSpinner.SpinnerState.Running;
var progress = new Progress<double>((d) => { });
var akiReleaseInfoFile = await DownloadCacheHelper.DownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl, progress);
var akiReleaseInfoFile =
await DownloadCacheHelper.DownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress);
if (akiReleaseInfoFile == null)
{
InstallButtonText = "Could not get SPT release metadata";
InstallButtonCheckState = StatusSpinner.SpinnerState.Error;
return;
}
var akiReleaseInfo = JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(akiReleaseInfoFile.FullName));
var akiReleaseInfo =
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(akiReleaseInfoFile.FullName));
if (akiReleaseInfo == null)
{
InstallButtonText = "Could not parse latest SPT release";
InstallButtonCheckState = StatusSpinner.SpinnerState.Error;
return;
}
InstallButtonText = $"Start Install: SPT v{akiReleaseInfo.AkiVersion}";
InstallButtonCheckState = StatusSpinner.SpinnerState.OK;
AllowDetailsButton = true;
AllowInstall = result.Succeeded;
});
Task.Run(() =>
{
CacheInfoText = "Getting cache size ...";
CacheCheckState = StatusSpinner.SpinnerState.Running;
CacheInfoText = $"Cache Size: {DownloadCacheHelper.GetCacheSizeText()}";
CacheCheckState = StatusSpinner.SpinnerState.OK;
});

View File

@ -7,11 +7,11 @@ namespace SPTInstaller.ViewModels;
public class ViewModelBase : ReactiveObject, IActivatableViewModel, IRoutableViewModel
{
public ViewModelActivator Activator { get; } = new();
public string? UrlPathSegment => Guid.NewGuid().ToString().Substring(0, 7);
public IScreen HostScreen { get; }
/// <summary>
/// Delay the return of the viewmodel
/// </summary>
@ -21,10 +21,10 @@ public class ViewModelBase : ReactiveObject, IActivatableViewModel, IRoutableVie
private async Task<ViewModelBase> WithDelay(int Milliseconds)
{
await Task.Delay(Milliseconds);
return this;
}
/// <summary>
/// Navigate to another viewmodel after a delay
/// </summary>
@ -38,19 +38,16 @@ public class ViewModelBase : ReactiveObject, IActivatableViewModel, IRoutableVie
HostScreen.Router.Navigate.Execute(await ViewModel.WithDelay(Milliseconds));
});
}
/// <summary>
/// Navigate to another viewmodel
/// </summary>
/// <param name="ViewModel"></param>
public void NavigateTo(ViewModelBase ViewModel)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
HostScreen.Router.Navigate.Execute(ViewModel);
});
Dispatcher.UIThread.InvokeAsync(() => { HostScreen.Router.Navigate.Execute(ViewModel); });
}
public ViewModelBase(IScreen Host)
{
HostScreen = Host;

View File

@ -2,24 +2,22 @@
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"
xmlns:cc="using:SPTInstaller.CustomControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.InstallView">
<Grid ColumnDefinitions="*, 2*">
<cc:ProgressableTaskList Tasks="{Binding MyTasks}"
Padding="20"
Background="{StaticResource AKI_Background_Dark}"
PendingColor="Gray"
RunningColor="DodgerBlue"
CompletedColor="{StaticResource AKI_Brush_Yellow}"
/>
<cc:TaskDetails Grid.Column="1"
Message="{Binding CurrentTask.StatusMessage}"
Details="{Binding CurrentTask.StatusDetails}"
Progress="{Binding CurrentTask.Progress}"
IndeterminateProgress="{Binding CurrentTask.IndeterminateProgress}"
ShowProgress="{Binding CurrentTask.ShowProgress}"
/>
</Grid>
</UserControl>
<Grid ColumnDefinitions="*, 2*">
<cc:ProgressableTaskList Tasks="{Binding MyTasks}"
Padding="20"
Background="{StaticResource AKI_Background_Dark}"
PendingColor="Gray"
RunningColor="DodgerBlue"
CompletedColor="{StaticResource AKI_Brush_Yellow}" />
<cc:TaskDetails Grid.Column="1"
Message="{Binding CurrentTask.StatusMessage}"
Details="{Binding CurrentTask.StatusDetails}"
Progress="{Binding CurrentTask.Progress}"
IndeterminateProgress="{Binding CurrentTask.IndeterminateProgress}"
ShowProgress="{Binding CurrentTask.ShowProgress}" />
</Grid>
</UserControl>

View File

@ -1,14 +1,14 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:SPTInstaller.ViewModels"
xmlns:rxui="using:Avalonia.ReactiveUI"
xmlns:rxui="using:Avalonia.ReactiveUI"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cc="using:SPTInstaller.CustomControls"
xmlns:cc="using:SPTInstaller.CustomControls"
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.MainWindow"
Icon="/Assets/icon.ico"
Icon="/Assets/icon.ico"
Title="SPT Installer"
Height="450" Width="750"
WindowStartupLocation="CenterScreen"
@ -16,26 +16,24 @@
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
Background="{StaticResource AKI_Background_Light}"
MinWidth="800" MinHeight="400"
>
<Window.Styles>
<StyleInclude Source="/Assets/Styles.axaml"/>
</Window.Styles>
MinWidth="800" MinHeight="400">
<Window.Styles>
<StyleInclude Source="/Assets/Styles.axaml" />
</Window.Styles>
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
<Grid RowDefinitions="AUTO,*">
<cc:TitleBar Title="{Binding Title}"
XButtonCommand="{Binding CloseCommand}"
MinButtonCommand="{Binding MinimizeCommand}"
/>
<Grid RowDefinitions="AUTO,*">
<cc:TitleBar Title="{Binding Title}"
XButtonCommand="{Binding CloseCommand}"
MinButtonCommand="{Binding MinimizeCommand}" />
<dialogHost:DialogHost Grid.Row="1" Background="{StaticResource AKI_Background_Light}">
<rxui:RoutedViewHost Router="{Binding Router}"/>
<dialogHost:DialogHost Grid.Row="1" Background="{StaticResource AKI_Background_Light}">
<rxui:RoutedViewHost Router="{Binding Router}" />
</dialogHost:DialogHost>
</Grid>
</Window>
</Grid>
</Window>

View File

@ -4,43 +4,39 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cc="using:SPTInstaller.CustomControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.MessageView"
>
<UserControl.Styles>
<Style Selector="Grid.error">
<Setter Property="Background" Value="#330000"/>
</Style>
<Style Selector="Label.error">
<Setter Property="Foreground" Value="Crimson"/>
</Style>
</UserControl.Styles>
x:Class="SPTInstaller.Views.MessageView">
<UserControl.Styles>
<Style Selector="Grid.error">
<Setter Property="Background" Value="#330000" />
</Style>
<Style Selector="Label.error">
<Setter Property="Foreground" Value="Crimson" />
</Style>
<Grid ColumnDefinitions="*,AUTO,*" RowDefinitions="*,AUTO,20,AUTO,*"
Classes.error="{Binding HasErrors}">
</UserControl.Styles>
<Label Grid.Column="1" Grid.Row="1"
Classes.error="{Binding HasErrors}">
<TextBlock Text="{Binding Message}" FontSize="18"
TextWrapping="Wrap"
MaxWidth="500"
HorizontalAlignment="Center"
/>
</Label>
<Grid ColumnDefinitions="*,AUTO,*" RowDefinitions="*,AUTO,20,AUTO,*"
Classes.error="{Binding HasErrors}">
<Button Grid.Column="1" Grid.Row="3"
Content="Close" Command="{Binding CloseCommand}"
FontSize="15" FontWeight="SemiBold"
Classes.yellow="{Binding !HasErrors}"
IsVisible="{Binding ShowCloseButton}"
HorizontalAlignment="Center"
VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
Padding="20 10"
/>
<Label Grid.Column="1" Grid.Row="1"
Classes.error="{Binding HasErrors}">
<TextBlock Text="{Binding Message}" FontSize="18"
TextWrapping="Wrap"
MaxWidth="500"
HorizontalAlignment="Center" />
</Label>
<Button Grid.Column="1" Grid.Row="3"
Content="Close" Command="{Binding CloseCommand}"
FontSize="15" FontWeight="SemiBold"
Classes.yellow="{Binding !HasErrors}"
IsVisible="{Binding ShowCloseButton}"
HorizontalAlignment="Center"
VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
Padding="20 10" />
<cc:CacheInfo Grid.Row="4" Grid.ColumnSpan="3" Padding="10" Margin="10 0 0 0"
VerticalAlignment="Bottom"
InfoText="{Binding CacheInfoText}" State="{Binding CacheCheckState}"
/>
</Grid>
</UserControl>
InfoText="{Binding CacheInfoText}" State="{Binding CacheCheckState}" />
</Grid>
</UserControl>

View File

@ -2,39 +2,48 @@
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"
xmlns:cc="using:SPTInstaller.CustomControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.PreChecksView">
<!-- main UI grid -->
<Grid RowDefinitions="10, *, 10, Auto, 10" ColumnDefinitions="10, *, Auto, 10">
<!-- info card -->
<Border Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" BoxShadow="1 1 10 0 black" CornerRadius="15">
<!-- main information grid -->
<Grid RowDefinitions="10, Auto, *, Auto, 10" ColumnDefinitions="10, 2*, Auto,*, 10">
<Label Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center"
Content="Details" FontSize="20"
/>
<Label Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center"
Content="Details" FontSize="20" Margin="0 2 0 0"/>
<!-- selected precheck details grid -->
<cc:PreCheckDetails Grid.Row="2" Grid.Column="1"
<cc:PreCheckDetails Grid.Row="2" Grid.Column="1"
PreChecks="{Binding PreChecks}"
HasSelection="{Binding HasPreCheckSelected}"
/>
HasSelection="{Binding HasPreCheckSelected}" />
<!-- info card vertical separator -->
<Rectangle Grid.Row="1" Grid.RowSpan="2" Grid.Column="2" VerticalAlignment="Stretch"
Fill="black" Width="1" Margin="10 0"
/>
Fill="black" Width="1" Margin="10 0" />
<!-- precheck list -->
<Label Grid.Row="1" Grid.Column="3" HorizontalAlignment="Center"
Content="Pre-Checks" FontSize="20"
<Grid Grid.Row="1" Grid.Column="3">
<Label HorizontalAlignment="Center"
Margin="0 2 0 0"
Content="Pre-Checks" FontSize="20"
/>
<Button Padding="10" x:Name="debugBtn"
HorizontalAlignment="Right" VerticalAlignment="Top"
Classes="icon"
Command="{Binding LaunchWithDebug}"
IsVisible="{Binding !Debugging}"
>
<Path Data="{StaticResource Bug}" Fill="{Binding ElementName=debugBtn, Path=Foreground}"
/>
</Button>
</Grid>
<ItemsControl ItemsSource="{Binding PreChecks}" Grid.Row="2" Grid.Column="3">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@ -43,36 +52,33 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<cc:PreCheckItem PreCheckName="{Binding Name}"
IsRequired="{Binding IsRequired}"
IsSelected="{Binding IsSelected}"
State="{Binding State}"
SelectCommand="{Binding $parent[ItemsControl].DataContext.SelectPreCheckCommand}"
HorizontalAlignment="Stretch"
/>
<cc:PreCheckItem PreCheckName="{Binding Name}"
IsRequired="{Binding IsRequired}"
IsSelected="{Binding IsSelected}"
State="{Binding State}"
SelectCommand="{Binding $parent[ItemsControl].DataContext.SelectPreCheckCommand}"
HorizontalAlignment="Stretch" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- cache info -->
<cc:CacheInfo Grid.Row="2" Grid.Column="3" Padding="10"
VerticalAlignment="Bottom" HorizontalAlignment="Left"
InfoText="{Binding CacheInfoText}" State="{Binding CacheCheckState}"
/>
InfoText="{Binding CacheInfoText}" State="{Binding CacheCheckState}" />
</Grid>
</Border>
<!-- Install path info -->
<StackPanel Grid.Row="3" Grid.Column="1" >
<StackPanel Grid.Row="3" Grid.Column="1">
<Label Content="Install Path" FontSize="20" />
<TextBlock TextWrapping="Wrap" Margin="3 0"
Text="{Binding InstallPath}"
Foreground="DodgerBlue" FontWeight="SemiBold"
/>
Foreground="DodgerBlue" FontWeight="SemiBold" />
</StackPanel>
<!-- Start install button -->
<Button Grid.Column="2" Grid.Row="3" Padding="20 10"
IsVisible="{Binding !UpdateInfo.Show}"
@ -80,14 +86,14 @@
FontSize="15" FontWeight="SemiBold"
Classes="yellow"
Command="{Binding StartInstallCommand}"
CornerRadius="15"
>
CornerRadius="15">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding InstallButtonText}" VerticalAlignment="Center" Foreground="Black"/>
<cc:StatusSpinner State="{Binding InstallButtonCheckState}" Margin="2" IsVisible="{Binding !AllowInstall}"/>
<TextBlock Text="{Binding InstallButtonText}" VerticalAlignment="Center" Foreground="Black" />
<cc:StatusSpinner State="{Binding InstallButtonCheckState}" Margin="2"
IsVisible="{Binding !AllowInstall}" />
</StackPanel>
</Button>
<!-- Update installer button -->
<cc:UpdateButton Grid.Column="2" Grid.Row="3"
IsVisible="{Binding UpdateInfo.Show}"
@ -97,9 +103,9 @@
Updating="{Binding UpdateInfo.Updating}"
DismissCommand="{Binding DismissUpdateCommand}"
UpdateCommand="{Binding UpdateInstallerCommand}"
WhatsNewCommand="{Binding WhatsNewCommand}"
DownloadProgress="{Binding UpdateInfo.DownloadProgress}"
UpdateAvailable="{Binding UpdateInfo.UpdateAvailable}"
CheckingForUpdate="{Binding UpdateInfo.CheckingForUpdates}"
/>
CheckingForUpdate="{Binding UpdateInfo.CheckingForUpdates}" />
</Grid>
</UserControl>
</UserControl>

View File

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="AvaloniaTest.Desktop"/>
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="AvaloniaTest.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@ -0,0 +1,9 @@
{
// You will want to remove comments before using this template
"latestVersion": "0.0", // the new version you are pushing
"changes": [ // a list of chnages. These will be formated for you. Don't add leading bullets or such
"changes",
"go",
"here"
]
}