Refactor, make safer, add some logging
This commit is contained in:
Connor Drahoss 2024-02-21 07:34:48 +01:00
parent b6181e174b
commit 4663bbca87
19 changed files with 67 additions and 150 deletions

View File

@ -2,7 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="VaultSmpInstaller.App"
x:DataType="viewModels:ThemeViewModel"
xmlns:local="using:VaultSmpInstaller"
xmlns:viewModels="clr-namespace:VaultSmpInstaller.ViewModels"
RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
@ -17,7 +16,7 @@
<Setter Property="Button.HorizontalAlignment" Value="Center" />
</Style>
<Style Selector="Button">
<Setter Property="Button.Background" Value="{Binding ButtonBackground}"/>
<Setter Property="Background" Value="{Binding ButtonBackground}"/>
</Style>
</Application.Styles>

View File

@ -6,7 +6,7 @@ using VaultSmpInstaller.Views;
namespace VaultSmpInstaller;
public partial class App : Application
public class App : Application
{
public override void Initialize()
{

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
namespace VaultSmpInstaller.Data;

View File

@ -10,7 +10,4 @@ namespace VaultSmpInstaller.Data;
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, ModProfile>))]
public partial class JsonContext: JsonSerializerContext
{
}
public partial class JsonContext: JsonSerializerContext;

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
using System.Text.Json.Serialization;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
namespace VaultSmpInstaller.Data;
[JsonSerializable(typeof(ModProfile))]
public class ModProfile
{
[JsonPropertyName("required")]

View File

@ -1,11 +0,0 @@
using Avalonia.Media;
namespace VaultSmpInstaller.ViewModels;
public class DownloadingWindowViewModel : ViewModelBase
{
public static Brush Background => SolidColorBrush.Parse("#282A36");
public static Brush SecondaryBackground => SolidColorBrush.Parse("#44475A");
public static Brush ButtonBackground => SolidColorBrush.Parse("#6272A4");
public static Brush TextColor => SolidColorBrush.Parse("#F8F8F2");
}

View File

@ -1,15 +1,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reactive.Linq;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Media;
using ReactiveUI;
@ -17,7 +10,7 @@ using VaultSmpInstaller.Data;
namespace VaultSmpInstaller.ViewModels;
public class MainWindowViewModel : ViewModelBase
public class MainWindowViewModel : ReactiveObject
{
public static Brush Background => SolidColorBrush.Parse("#282A36");
public static Brush SecondaryBackground => SolidColorBrush.Parse("#44475A");
@ -46,7 +39,7 @@ public class MainWindowViewModel : ViewModelBase
if (File.Exists(Path.Combine(SelectedInstance.InstancePath, "installedInstance.json")))
{
await using FileStream fs = new FileStream(Path.Combine(SelectedInstance.InstancePath, "installedInstance.json"), FileMode.Open);
InstalledInstanceConfig = await JsonSerializer.DeserializeAsync<InstanceConfig>(fs, JsonContext.Default.InstanceConfig);
InstalledInstanceConfig = await JsonSerializer.DeserializeAsync(fs, JsonContext.Default.InstanceConfig);
if (InstalledInstanceConfig != null)
{
InstalledInstanceConfig.InstancePath = SelectedInstance.InstancePath;
@ -66,8 +59,8 @@ public class MainWindowViewModel : ViewModelBase
});
}
public InstanceConfig? InstalledInstanceConfig { get; set; } = null;
public InstanceConfig? LatestInstanceConfig { get; set; } = null;
public InstanceConfig? InstalledInstanceConfig { get; set; }
public InstanceConfig? LatestInstanceConfig { get; set; }
public Dictionary<string, ModProfile> EnabledModProfiles => LatestInstanceConfig?.ModProfiles.Where(pair => pair.Value.IsEnabled).ToDictionary() ?? new Dictionary<string, ModProfile>();
public List<string> EnabledModProfileNames => EnabledModProfiles.Keys.ToList();
@ -75,7 +68,7 @@ public class MainWindowViewModel : ViewModelBase
public List<string> DisabledModProfileNames => DisabledModProfiles.Keys.ToList();
public Dictionary<string, ModProfile> InstalledModProfiles => InstalledInstanceConfig?.ModProfiles.Where(pair => pair.Value.IsEnabled).ToDictionary() ?? new Dictionary<string, ModProfile>();
public List<string> InstalledModProfileNames => InstalledModProfiles.Keys.ToList();
public List<string> InstalledModProfileNames => InstalledModProfiles.Keys.ToList();
public string SelectedInstanceName => SelectedInstance == null ? "No profile selected" : $"Selected Profile: {SelectedInstance.InstanceName}";
public ProfileWindow2ViewModel.InstanceInfo? SelectedInstance { get; set; }

View File

@ -9,7 +9,7 @@ using ReactiveUI;
namespace VaultSmpInstaller.ViewModels;
public class ProfileWindow1ViewModel : ViewModelBase
public class ProfileWindow1ViewModel : ReactiveObject
{
public Interaction<ProfileWindow2ViewModel, ProfileWindow2ViewModel.InstanceInfo?> ShowProfileSelectionDialog { get; }
@ -25,12 +25,15 @@ public class ProfileWindow1ViewModel : ViewModelBase
UseCurseforgeCommand = ReactiveCommand.CreateFromTask(async () =>
{
if (curseforgeDir == null) return null;
var profileWindowModel = new ProfileWindow2ViewModel(ProfileWindow2ViewModel.InstanceType.Curseforge, curseforgeDir);
return await ShowProfileSelectionDialog.Handle(profileWindowModel);
});
UsePrismCommand = ReactiveCommand.CreateFromTask(async () =>
{
if (prismDir == null) return null;
var profileWindowModel = new ProfileWindow2ViewModel(ProfileWindow2ViewModel.InstanceType.Prism, prismDir);
return await ShowProfileSelectionDialog.Handle(profileWindowModel);
});
@ -41,8 +44,8 @@ public class ProfileWindow1ViewModel : ViewModelBase
public static Brush ButtonBackground => SolidColorBrush.Parse("#6272A4");
public static Brush TextColor => SolidColorBrush.Parse("#F8F8F2");
private string? _curseforgeInstanceDir = null;
public bool IsCurseforgeInstalled { get; set; } = false;
private string? _curseforgeInstanceDir;
public bool IsCurseforgeInstalled { get; set; }
public string CurseforgeButtonText => IsCurseforgeInstalled ? "Curseforge" : "Curseforge Not Detected";
@ -61,9 +64,9 @@ public class ProfileWindow1ViewModel : ViewModelBase
}
}
public bool IsPrismInstalled { get; set; } = false;
public bool IsPrismInstalled { get; set; }
private string? _prismInstanceDir = null;
private string? _prismInstanceDir;
public string PrismButtonText => IsPrismInstalled ? "Prism Launcher" : "Prism Launcher Not Detected";
public string? PrismInstanceDir
@ -81,7 +84,7 @@ public class ProfileWindow1ViewModel : ViewModelBase
}
}
public bool IsOverwolfInstalled { get; set; } = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Overwolf", "UninstallString", null) != null;
public bool IsOverwolfInstalled { get; } = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Overwolf", "UninstallString", null) != null;
public bool TryGetCurseforgeMinecraftRoot(out string? minecraftRoot)
{
@ -91,7 +94,12 @@ public class ProfileWindow1ViewModel : ViewModelBase
if (!File.Exists(Path.Combine(appData, "Curseforge", "storage.json"))) return false;
var curseforgeConfig = JsonNode.Parse(File.ReadAllText(Path.Combine(appData, "Curseforge", "storage.json")))!.AsObject();
var curseforgeConfig = JsonNode.Parse(File.ReadAllText(Path.Combine(appData, "Curseforge", "storage.json")))?.AsObject();
if (curseforgeConfig == null)
{
File.AppendAllText(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "smpinstaller.log"), "Failed to read curseforge config!\n");
return false;
}
if (!curseforgeConfig.TryGetPropertyValue("minecraft-settings", out var minecraftSettingsNode))
{
minecraftRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "curseforge", "minecraft", "Instances");
@ -132,61 +140,4 @@ public class ProfileWindow1ViewModel : ViewModelBase
}
return false;
}
// public bool TryGetOverwolfMinecraftRoot(out string? minecraftRoot)
// {
// minecraftRoot = null;
//
// var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
// var overwolfDbDirectory = Path.Combine(localAppData, "Overwolf", "BrowserCache", "Local Storage", "leveldb");
//
// if (!Directory.Exists(overwolfDbDirectory)) return false;
//
// var overwolfDb = new Database(new(overwolfDbDirectory), false, new Options() { ReadOnly = true});
// byte[] databaseKey = new byte[]
// {
// 0x5F, 0x6F,
// 0x76, 0x65, 0x72, 0x77, 0x6F, 0x6C, 0x66, 0x2D,
// 0x65, 0x78, 0x74, 0x65, 0x6E, 0x73, 0x69, 0x6F,
// 0x6E, 0x3A, 0x2F, 0x2F, 0x63, 0x63, 0x68, 0x68,
// 0x63, 0x61, 0x69, 0x61, 0x70, 0x65, 0x69, 0x6B,
// 0x6A, 0x62, 0x64, 0x62, 0x70, 0x66, 0x70, 0x6C,
// 0x67, 0x6D, 0x70, 0x6F, 0x62, 0x62, 0x63, 0x64,
// 0x6B, 0x64, 0x61, 0x70, 0x68, 0x63, 0x6C, 0x62,
// 0x6D, 0x6B, 0x62, 0x6A, 0x00, 0x01, 0x6D, 0x69,
// 0x6E, 0x65, 0x63, 0x72, 0x61, 0x66, 0x74, 0x2D,
// 0x73, 0x65, 0x74, 0x74, 0x69, 0x6E, 0x67, 0x73
// };
// String? curseforgeMinecraftSettings = "";
// try
// {
// overwolfDb.Open();
// curseforgeMinecraftSettings = Encoding.ASCII.GetString(overwolfDb.Get(databaseKey))[1..];
// }
// catch (NullReferenceException e)
// {
// minecraftRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "curseforge", "minecraft", "Instances");
// return true;
// }
// finally
// {
// overwolfDb.Close();
// overwolfDb.Dispose();
// }
// if(String.IsNullOrEmpty(curseforgeMinecraftSettings))
// {
// minecraftRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "curseforge", "minecraft", "Instances");
// return true;
// }
//
// var minecraftSettings = JsonNode.Parse(curseforgeMinecraftSettings);
// if (!minecraftSettings!.AsObject().TryGetPropertyValue("minecraftRoot", out var minecraftRootNode)) {
// minecraftRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "curseforge", "minecraft", "Instances");
// return true;
// }
// if (!minecraftRootNode!.AsValue().TryGetValue(out minecraftRoot)) return false;
//
// minecraftRoot = Path.Combine(minecraftRoot, "Instances");
// return true;
// }
}

View File

@ -10,11 +10,10 @@ using ReactiveUI;
namespace VaultSmpInstaller.ViewModels;
public class ProfileWindow2ViewModel : ViewModelBase
public class ProfileWindow2ViewModel : ReactiveObject
{
public Interaction<ProfileWindow2ViewModel, ProfileWindow2ViewModel.InstanceInfo?> ShowProfileSelectionDialog { get; }
public ReactiveCommand<Unit, ProfileWindow2ViewModel.InstanceInfo?> SelectProfileCommand { get; }
public ReactiveCommand<Unit, InstanceInfo?> SelectProfileCommand { get; }
public static Brush Background => SolidColorBrush.Parse("#282A36");
public static Brush SecondaryBackground => SolidColorBrush.Parse("#44475A");
@ -37,6 +36,7 @@ public class ProfileWindow2ViewModel : ViewModelBase
foreach (var directory in Directory.EnumerateDirectories(instancesDir))
{
if (!File.Exists(Path.Combine(directory, "instance.cfg"))) continue;
foreach(String line in File.ReadLines(Path.Combine(directory, "instance.cfg")))
{
if (line.StartsWith("name"))
@ -56,12 +56,17 @@ public class ProfileWindow2ViewModel : ViewModelBase
}
}
break;
case InstanceType.Curseforge: case InstanceType.Overwolf:
case InstanceType.Curseforge:
foreach (var directory in Directory.EnumerateDirectories(instancesDir))
{
if (!File.Exists(Path.Combine(directory, "minecraftinstance.json"))) continue;
string? instanceConfigName = null;
if (File.Exists(Path.Combine(directory, "minecraftinstance.json"))) instanceConfigName = "minecraftinstance.json";
if (File.Exists(Path.Combine(directory, "minecraftInstance.json"))) instanceConfigName = "minecraftInstance.json";
if (instanceConfigName == null) continue;
var instanceConfig = JsonNode.Parse(File.ReadAllText(Path.Combine(directory, "minecraftinstance.json")))!.AsObject();
var instanceConfig = JsonNode.Parse(File.ReadAllText(Path.Combine(directory, instanceConfigName)))?.AsObject();
if(instanceConfig == null) continue;
if (!instanceConfig.TryGetPropertyValue("name", out var nameNode)) continue;
if (!nameNode!.AsValue().TryGetValue(out string? instanceName)) continue;
@ -88,7 +93,7 @@ public class ProfileWindow2ViewModel : ViewModelBase
public enum InstanceType
{
Prism, Curseforge, Overwolf
Prism, Curseforge
}
public record InstanceInfo(String InstanceName, String InstancePath, String MinecraftPath, String ModsPath, String ConfigPath, String ScriptsPath);

View File

@ -1,8 +1,9 @@
using Avalonia.Media;
using ReactiveUI;
namespace VaultSmpInstaller.ViewModels;
public class ThemeViewModel : ViewModelBase
public class ThemeViewModel : ReactiveObject
{
public static Brush Background => SolidColorBrush.Parse("#282A36");
public static Brush SecondaryBackground => SolidColorBrush.Parse("#44475A");

View File

@ -1,7 +0,0 @@
using ReactiveUI;
namespace VaultSmpInstaller.ViewModels;
public class ViewModelBase : ReactiveObject
{
}

View File

@ -6,14 +6,14 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia.ProgressRing"
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="160"
x:Class="VaultSmpInstaller.Views.DownloadingWindow"
x:DataType="vm:DownloadingWindowViewModel"
x:DataType="vm:ThemeViewModel"
Icon="/Assets/icon.ico"
Title="Downloading Update"
Width="300" Height="160"
WindowStartupLocation="CenterOwner"
CanResize="False">
<Design.DataContext>
<vm:DownloadingWindowViewModel/>
<vm:ThemeViewModel/>
</Design.DataContext>
<StackPanel Background="{Binding Background}">
<TextBlock FontSize="18" Margin="10, 10, 10, 0">Downloading Latest Release</TextBlock>

View File

@ -1,6 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Controls;
namespace VaultSmpInstaller.Views;

View File

@ -1,14 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
namespace VaultSmpInstaller.Views;
public partial class InstanceNotIntactWindow : Window
{
private readonly MainWindow _mainWindow;
public InstanceNotIntactWindow() { }
private readonly MainWindow? _mainWindow;
public InstanceNotIntactWindow(MainWindow mainWindow)
{
InitializeComponent();
@ -18,12 +18,12 @@ public partial class InstanceNotIntactWindow : Window
private void Continue(object? sender, RoutedEventArgs e)
{
Dispatcher.UIThread.Invoke(Close);
_mainWindow.ContinueInstalling.Set();
_mainWindow?.ContinueInstalling.Set();
}
private void Cancel(object? sender, RoutedEventArgs e)
{
Dispatcher.UIThread.Invoke(Close);
Dispatcher.UIThread.Invoke(_mainWindow.Close);
if (_mainWindow != null) Dispatcher.UIThread.Invoke(_mainWindow.Close);
}
}

View File

@ -30,7 +30,7 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
private async Task ProcessInstance(String instancePath)
{
await using FileStream fs = new FileStream(Path.Combine(instancePath, "instance.json"), FileMode.Open);
ViewModel!.LatestInstanceConfig = await JsonSerializer.DeserializeAsync<InstanceConfig>(fs, JsonContext.Default.InstanceConfig);
ViewModel!.LatestInstanceConfig = await JsonSerializer.DeserializeAsync(fs, JsonContext.Default.InstanceConfig);
if (ViewModel!.LatestInstanceConfig != null)
{
ViewModel!.LatestInstanceConfig.InstancePath = instancePath;
@ -68,20 +68,19 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
private void StartDownload()
{
var downloadThread = new Thread(async (arg) =>
var downloadThread = new Thread((arg) =>
{
if (arg is CancellationTokenSource)
if (arg is CancellationTokenSource tokenSource)
{
var tokenSource = (CancellationTokenSource)arg;
var archivePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".zip");
var extractPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
try
{
HttpClient httpClient = new HttpClient();
using var response = await httpClient.GetAsync("https://holenode.cdnbcn.net/LatestProfile.zip", tokenSource.Token);
await using var fs = new FileStream(archivePath, FileMode.OpenOrCreate);
await response.Content.CopyToAsync(fs, tokenSource.Token);
using var response = httpClient.GetAsync("https://holenode.cdnbcn.net/LatestProfile.zip", tokenSource.Token).Result;
using var fs = new FileStream(archivePath, FileMode.OpenOrCreate);
response.Content.CopyTo(fs, null, tokenSource.Token);
fs.Close();
@ -91,16 +90,16 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
File.Delete(archivePath);
await Dispatcher.UIThread.Invoke(() => ProcessInstance(extractPath));
Dispatcher.UIThread.Invoke(() => ProcessInstance(extractPath));
}
catch (OperationCanceledException e)
catch (OperationCanceledException)
{
try
{
File.Delete(archivePath);
Directory.Delete(extractPath, true);
}
catch (Exception ignored)
catch (Exception)
{
// ignored
}
@ -112,7 +111,7 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
CancellationTokenSource source = new CancellationTokenSource();
downloadThread.Start(source);
_downloadingWindow.DataContext = new DownloadingWindowViewModel();
_downloadingWindow.DataContext = new ThemeViewModel();
_downloadingWindow.Closed += (_, _) => source.Cancel();
_downloadingWindow.ShowDialog(this);
}

View File

@ -1,7 +1,5 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
using VaultSmpInstaller.ViewModels;
@ -17,9 +15,9 @@ public partial class ProfileWindow2 : ReactiveWindow<ProfileWindow2ViewModel>
this.WhenActivated(action => action(ViewModel!.SelectProfileCommand.Subscribe(Close)));
}
private void SelectingItemsControl_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
private void SelectingItemsControl_OnSelectionChanged(object? _, SelectionChangedEventArgs e)
{
ViewModel!.SelectedInstance = ViewModel.Instances[((string)e.AddedItems[0]!)!];
ViewModel!.SelectedInstance = ViewModel.Instances[(string)e.AddedItems[0]!];
ViewModel.RaisePropertyChanged(nameof(ViewModel.IsInstanceSelected));
ViewModel.RaisePropertyChanged(nameof(ViewModel.SelectedInstance));
}

View File

@ -3,7 +3,6 @@
xmlns:vm="using:VaultSmpInstaller.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400"
x:Class="VaultSmpInstaller.Views.RemoveOverwolfWindow"
x:DataType="vm:ThemeViewModel"

View File

@ -1,13 +1,9 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Microsoft.Win32;
namespace VaultSmpInstaller.Views;

View File

@ -1,14 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
namespace VaultSmpInstaller.Views;
public partial class SuccessWindow : Window
{
private readonly MainWindow _mainWindow;
public SuccessWindow() { }
private readonly MainWindow? _mainWindow;
public SuccessWindow(MainWindow mainWindow)
{
InitializeComponent();
@ -17,7 +17,7 @@ public partial class SuccessWindow : Window
private void Ok(object? sender, RoutedEventArgs e)
{
_mainWindow.ContinueInstalling.Set();
_mainWindow?.ContinueInstalling.Set();
Dispatcher.UIThread.Invoke(Close);
}
}