diff --git a/App.axaml b/App.axaml
index 4255230..69841f4 100644
--- a/App.axaml
+++ b/App.axaml
@@ -1,15 +1,24 @@
+ xmlns:viewModels="clr-namespace:VaultSmpInstaller.ViewModels"
+ RequestedThemeVariant="Dark">
-
-
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Assets/avalonia-logo.ico b/Assets/avalonia-logo.ico
deleted file mode 100644
index da8d49f..0000000
Binary files a/Assets/avalonia-logo.ico and /dev/null differ
diff --git a/Assets/icon.ico b/Assets/icon.ico
new file mode 100644
index 0000000..cafb357
Binary files /dev/null and b/Assets/icon.ico differ
diff --git a/Data/InstanceConfig.cs b/Data/InstanceConfig.cs
new file mode 100644
index 0000000..a5d88ff
--- /dev/null
+++ b/Data/InstanceConfig.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace VaultSmpInstaller.Data;
+
+public class InstanceConfig
+{
+ [JsonPropertyName("version")]
+ public string Version { get; set; }
+ [JsonPropertyName("modProfiles")]
+ public Dictionary ModProfiles { get; set; }
+ [JsonPropertyName("replaceFiles")]
+ public Dictionary ReplaceFiles { get; set; }
+
+ [JsonIgnore]
+ public string InstancePath { get; set; }
+}
\ No newline at end of file
diff --git a/Data/JsonContext.cs b/Data/JsonContext.cs
new file mode 100644
index 0000000..9928376
--- /dev/null
+++ b/Data/JsonContext.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace VaultSmpInstaller.Data;
+
+[JsonSourceGenerationOptions(WriteIndented = true, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip)]
+[JsonSerializable(typeof(InstanceConfig))]
+[JsonSerializable(typeof(ModProfile))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(Dictionary))]
+public partial class JsonContext: JsonSerializerContext
+{
+
+}
\ No newline at end of file
diff --git a/Data/ModProfile.cs b/Data/ModProfile.cs
new file mode 100644
index 0000000..ce962d5
--- /dev/null
+++ b/Data/ModProfile.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
+
+namespace VaultSmpInstaller.Data;
+
+[JsonSerializable(typeof(ModProfile))]
+public class ModProfile
+{
+ [JsonPropertyName("required")]
+ public bool Required { get; set; }
+ [JsonPropertyName("defaultState")]
+ public bool DefaultState { get; set; }
+ [JsonPropertyName("mods")]
+ public Dictionary Mods { get; set; }
+ [JsonPropertyName("replaces")]
+ public Dictionary Replaces { get; set; }
+
+ [JsonPropertyName("enabled")]
+ public bool IsEnabled { get; set; }
+}
\ No newline at end of file
diff --git a/VaultSmpInstaller.csproj b/VaultSmpInstaller.csproj
index ae4da84..2d0f054 100644
--- a/VaultSmpInstaller.csproj
+++ b/VaultSmpInstaller.csproj
@@ -6,7 +6,14 @@
true
app.manifest
true
+
+ true
+ Assets\icon.ico
+
+
+
+
@@ -22,5 +29,6 @@
+
diff --git a/ViewLocator.cs b/ViewLocator.cs
deleted file mode 100644
index b96d46e..0000000
--- a/ViewLocator.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using System;
-using Avalonia.Controls;
-using Avalonia.Controls.Templates;
-using VaultSmpInstaller.ViewModels;
-
-namespace VaultSmpInstaller;
-
-public class ViewLocator : IDataTemplate
-{
- public Control? Build(object? data)
- {
- if (data is null)
- return null;
-
- var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
- var type = Type.GetType(name);
-
- if (type != null)
- {
- var control = (Control)Activator.CreateInstance(type)!;
- control.DataContext = data;
- return control;
- }
-
- return new TextBlock { Text = "Not Found: " + name };
- }
-
- public bool Match(object? data)
- {
- return data is ViewModelBase;
- }
-}
\ No newline at end of file
diff --git a/ViewModels/DownloadingWindowViewModel.cs b/ViewModels/DownloadingWindowViewModel.cs
new file mode 100644
index 0000000..8e3b968
--- /dev/null
+++ b/ViewModels/DownloadingWindowViewModel.cs
@@ -0,0 +1,11 @@
+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");
+}
\ No newline at end of file
diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs
index 2136be1..f2056d4 100644
--- a/ViewModels/MainWindowViewModel.cs
+++ b/ViewModels/MainWindowViewModel.cs
@@ -1,8 +1,111 @@
-namespace VaultSmpInstaller.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+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;
+using VaultSmpInstaller.Data;
+
+namespace VaultSmpInstaller.ViewModels;
public class MainWindowViewModel : ViewModelBase
{
-#pragma warning disable CA1822 // Mark members as static
- public string Greeting => "Welcome to Avalonia!";
-#pragma warning restore CA1822 // Mark members as static
+ 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");
+
+ public Interaction ShowProfileSelectionDialog { get; }
+
+ public ICommand SelectProfileCommand { get; }
+
+ public MainWindowViewModel()
+ {
+ ShowProfileSelectionDialog = new Interaction();
+
+ SelectProfileCommand = ReactiveCommand.CreateFromTask(async () =>
+ {
+ var profileWindowModel = new ProfileWindow1ViewModel();
+ SelectedInstance = await ShowProfileSelectionDialog.Handle(profileWindowModel);
+
+ this.RaisePropertyChanged(nameof(SelectedInstanceName));
+ this.RaisePropertyChanged(nameof(SelectedInstance));
+ this.RaisePropertyChanged(nameof(IsInstanceSelected));
+
+ if (SelectedInstance != null)
+ {
+ 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(fs, JsonContext.Default.InstanceConfig);
+ if (InstalledInstanceConfig != null)
+ {
+ InstalledInstanceConfig.InstancePath = SelectedInstance.InstancePath;
+ InstalledVersion = InstalledInstanceConfig.Version;
+ }
+ }
+ else
+ {
+ InstalledInstanceConfig = null;
+ InstalledVersion = "None";
+ }
+ this.RaisePropertyChanged(nameof(InstalledInstanceConfig));
+ this.RaisePropertyChanged(nameof(InstalledVersion));
+ this.RaisePropertyChanged(nameof(InstalledModProfiles));
+ this.RaisePropertyChanged(nameof(InstalledModProfileNames));
+ }
+ });
+ }
+
+ public InstanceConfig? InstalledInstanceConfig { get; set; } = null;
+ public InstanceConfig? LatestInstanceConfig { get; set; } = null;
+
+ public Dictionary EnabledModProfiles => LatestInstanceConfig?.ModProfiles.Where(pair => pair.Value.IsEnabled).ToDictionary() ?? new Dictionary();
+ public List EnabledModProfileNames => EnabledModProfiles.Keys.ToList();
+ public Dictionary DisabledModProfiles => LatestInstanceConfig?.ModProfiles.Where(pair => !pair.Value.IsEnabled).ToDictionary() ?? new Dictionary();
+ public List DisabledModProfileNames => DisabledModProfiles.Keys.ToList();
+
+ public Dictionary InstalledModProfiles => InstalledInstanceConfig?.ModProfiles.Where(pair => pair.Value.IsEnabled).ToDictionary() ?? new Dictionary();
+ public List InstalledModProfileNames => InstalledModProfiles.Keys.ToList();
+
+ public string SelectedInstanceName => SelectedInstance == null ? "No profile selected" : $"Selected Profile: {SelectedInstance.InstanceName}";
+ public ProfileWindow2ViewModel.InstanceInfo? SelectedInstance { get; set; }
+
+
+ private string _installedVersion = "None";
+ public string InstalledVersion
+ {
+ set
+ {
+ _installedVersion = value;
+ this.RaisePropertyChanged(nameof(IsLatestVersionText));
+ this.RaisePropertyChanged();
+ }
+
+ get => _installedVersion;
+ }
+
+ private string _latestVersion = "Downloading";
+ public string LatestVersion
+ {
+ set
+ {
+ _latestVersion = value;
+ this.RaisePropertyChanged(nameof(IsLatestVersionText));
+ this.RaisePropertyChanged();
+ }
+
+ get => _latestVersion;
+ }
+ public string IsLatestVersionText => InstalledVersion == LatestVersion ? "the latest version!" : "an outdated version.";
+ public bool IsInstanceSelected => SelectedInstance != null;
}
\ No newline at end of file
diff --git a/ViewModels/ProfileWindow1ViewModel.cs b/ViewModels/ProfileWindow1ViewModel.cs
new file mode 100644
index 0000000..4644be9
--- /dev/null
+++ b/ViewModels/ProfileWindow1ViewModel.cs
@@ -0,0 +1,126 @@
+using System;
+using System.IO;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Text.Json.Nodes;
+using Avalonia.Media;
+using ReactiveUI;
+
+namespace VaultSmpInstaller.ViewModels;
+
+public class ProfileWindow1ViewModel : ViewModelBase
+{
+ public Interaction ShowProfileSelectionDialog { get; }
+
+ public ReactiveCommand UseCurseforgeCommand { get; }
+ public ReactiveCommand UsePrismCommand { get; }
+
+ public ProfileWindow1ViewModel()
+ {
+ var curseforgeDir = CurseforgeInstanceDir;
+ var prismDir = PrismInstanceDir;
+
+ ShowProfileSelectionDialog = new Interaction();
+
+ UseCurseforgeCommand = ReactiveCommand.CreateFromTask(async () =>
+ {
+ var profileWindowModel = new ProfileWindow2ViewModel(ProfileWindow2ViewModel.InstanceType.Curseforge, curseforgeDir);
+ return await ShowProfileSelectionDialog.Handle(profileWindowModel);
+ });
+
+ UsePrismCommand = ReactiveCommand.CreateFromTask(async () =>
+ {
+ var profileWindowModel = new ProfileWindow2ViewModel(ProfileWindow2ViewModel.InstanceType.Prism, prismDir);
+ return await ShowProfileSelectionDialog.Handle(profileWindowModel);
+ });
+
+ }
+
+ 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");
+
+ private string? _curseforgeInstanceDir = null;
+ public bool IsCurseforgeInstalled { get; set; } = false;
+
+ public string CurseforgeButtonText => IsCurseforgeInstalled ? "Curseforge" : "Curseforge Not Detected";
+
+ public string? CurseforgeInstanceDir
+ {
+ get
+ {
+ if (_curseforgeInstanceDir == null && TryGetCurseforgeMinecraftRoot(out _curseforgeInstanceDir))
+ {
+ IsCurseforgeInstalled = true;
+ this.RaisePropertyChanged(nameof(IsCurseforgeInstalled));
+ this.RaisePropertyChanged(nameof(CurseforgeButtonText));
+ this.RaisePropertyChanged();
+ }
+ return _curseforgeInstanceDir;
+ }
+ }
+
+ public bool IsPrismInstalled { get; set; } = false;
+
+ private string? _prismInstanceDir = null;
+ public string PrismButtonText => IsPrismInstalled ? "Prism Launcher" : "Prism Launcher Not Detected";
+
+ public string? PrismInstanceDir
+ {
+ get
+ {
+ if (_prismInstanceDir == null && TryGetPrismMinecraftRoot(out _prismInstanceDir))
+ {
+ IsPrismInstalled = true;
+ this.RaisePropertyChanged(nameof(IsPrismInstalled));
+ this.RaisePropertyChanged(nameof(PrismButtonText));
+ this.RaisePropertyChanged();
+ }
+ return _prismInstanceDir;
+ }
+ }
+
+ public bool TryGetCurseforgeMinecraftRoot(out string? minecraftRoot)
+ {
+ minecraftRoot = null;
+
+ var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+
+ if (!File.Exists(Path.Combine(appData, "Curseforge", "storage.json"))) return false;
+
+ var curseforgeConfig = JsonNode.Parse(File.ReadAllText(Path.Combine(appData, "Curseforge", "storage.json")))!.AsObject();
+ if (!curseforgeConfig.TryGetPropertyValue("minecraft-settings", out var minecraftSettingsNode)) return false;
+ if (!minecraftSettingsNode!.AsValue().TryGetValue(out string? minecraftSettingsString)) return false;
+
+ var minecraftSettings = JsonNode.Parse(minecraftSettingsString);
+ if (!minecraftSettings!.AsObject().TryGetPropertyValue("minecraftRoot", out var minecraftRootNode)) return false;
+ if (!minecraftRootNode!.AsValue().TryGetValue(out minecraftRoot)) return false;
+
+ minecraftRoot = Path.Combine(minecraftRoot, "Instances");
+ return true;
+ }
+
+ public bool TryGetPrismMinecraftRoot(out string? minecraftRoot)
+ {
+ minecraftRoot = null;
+
+ var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+
+ if (!File.Exists(Path.Combine(appData, "PrismLauncher", "prismlauncher.cfg"))) return false;
+
+ foreach(String line in File.ReadLines(Path.Combine(appData, "PrismLauncher", "prismlauncher.cfg")))
+ {
+ if (line.StartsWith("InstanceDir"))
+ {
+ string path = line.Split('=', 2)[1];
+ if (Path.IsPathFullyQualified(path))
+ minecraftRoot = path;
+ else
+ minecraftRoot = Path.Combine(appData, "PrismLauncher", path);
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/ViewModels/ProfileWindow2ViewModel.cs b/ViewModels/ProfileWindow2ViewModel.cs
new file mode 100644
index 0000000..812d981
--- /dev/null
+++ b/ViewModels/ProfileWindow2ViewModel.cs
@@ -0,0 +1,95 @@
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reactive;
+using System.Text.Json.Nodes;
+using Avalonia.Media;
+using ReactiveUI;
+
+namespace VaultSmpInstaller.ViewModels;
+
+public class ProfileWindow2ViewModel : ViewModelBase
+{
+
+ public Interaction ShowProfileSelectionDialog { get; }
+ public ReactiveCommand SelectProfileCommand { get; }
+
+ 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");
+
+ public Dictionary Instances { get; }
+ public List InstanceNames { get; }
+
+ public InstanceInfo? SelectedInstance { get; set; } = null;
+
+ public Boolean IsInstanceSelected => SelectedInstance != null;
+
+ public ProfileWindow2ViewModel(InstanceType launcherType, string instancesDir)
+ {
+ Instances = new Dictionary();
+ switch (launcherType)
+ {
+ case InstanceType.Prism:
+ 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"))
+ {
+ Instances.Add(line.Split('=', 2)[1],
+ new InstanceInfo(
+ line.Split('=', 2)[1],
+ directory,
+ Path.Combine(directory, "minecraft"),
+ Path.Combine(directory, "minecraft", "mods"),
+ Path.Combine(directory, "minecraft", "config"),
+ Path.Combine(directory, "minecraft", "scripts")
+ )
+ );
+ break;
+ }
+ }
+ }
+ break;
+ case InstanceType.Curseforge:
+ foreach (var directory in Directory.EnumerateDirectories(instancesDir))
+ {
+ if (!File.Exists(Path.Combine(directory, "minecraftinstance.json"))) continue;
+
+ var instanceConfig = JsonNode.Parse(File.ReadAllText(Path.Combine(directory, "minecraftinstance.json")))!.AsObject();
+ if (!instanceConfig.TryGetPropertyValue("name", out var nameNode)) continue;
+ if (!nameNode!.AsValue().TryGetValue(out string? instanceName)) continue;
+
+ Instances.Add(instanceName,
+ new InstanceInfo(
+ instanceName,
+ directory,
+ directory,
+ Path.Combine(directory, "mods"),
+ Path.Combine(directory, "config"),
+ Path.Combine(directory, "scripts")
+ )
+ );
+ }
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(launcherType), launcherType, null);
+ }
+ InstanceNames = Instances.Keys.ToList();
+
+ SelectProfileCommand = ReactiveCommand.Create(() => SelectedInstance);
+ }
+
+
+ public enum InstanceType
+ {
+ Prism, Curseforge
+ }
+
+ public record InstanceInfo(String InstanceName, String InstancePath, String MinecraftPath, String ModsPath, String ConfigPath, String ScriptsPath);
+}
\ No newline at end of file
diff --git a/ViewModels/ThemeViewModel.cs b/ViewModels/ThemeViewModel.cs
new file mode 100644
index 0000000..618350b
--- /dev/null
+++ b/ViewModels/ThemeViewModel.cs
@@ -0,0 +1,11 @@
+using Avalonia.Media;
+
+namespace VaultSmpInstaller.ViewModels;
+
+public class ThemeViewModel : 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");
+}
\ No newline at end of file
diff --git a/Views/DownloadingWindow.axaml b/Views/DownloadingWindow.axaml
new file mode 100644
index 0000000..3c72f1d
--- /dev/null
+++ b/Views/DownloadingWindow.axaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Downloading Latest Release
+ This popup will close when complete
+
+
+
diff --git a/Views/DownloadingWindow.axaml.cs b/Views/DownloadingWindow.axaml.cs
new file mode 100644
index 0000000..1330e20
--- /dev/null
+++ b/Views/DownloadingWindow.axaml.cs
@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace VaultSmpInstaller.Views;
+
+public partial class DownloadingWindow : Window
+{
+ public DownloadingWindow()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/Views/InstanceNotIntactWindow.axaml b/Views/InstanceNotIntactWindow.axaml
new file mode 100644
index 0000000..f80c2c4
--- /dev/null
+++ b/Views/InstanceNotIntactWindow.axaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ This instance has been modified outside of this script.
+ If you have used the previous script this should be fine.
+ Otherwise I recommend making a new instance.
+ From this point, beware of issues ahead.
+
+
+
+
+
+
diff --git a/Views/InstanceNotIntactWindow.axaml.cs b/Views/InstanceNotIntactWindow.axaml.cs
new file mode 100644
index 0000000..b364495
--- /dev/null
+++ b/Views/InstanceNotIntactWindow.axaml.cs
@@ -0,0 +1,29 @@
+using Avalonia;
+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(MainWindow mainWindow)
+ {
+ InitializeComponent();
+ this._mainWindow = mainWindow;
+ }
+
+ private void Continue(object? sender, RoutedEventArgs e)
+ {
+ Dispatcher.UIThread.Invoke(Close);
+ _mainWindow.ContinueInstalling.Set();
+ }
+
+ private void Cancel(object? sender, RoutedEventArgs e)
+ {
+ Dispatcher.UIThread.Invoke(Close);
+ Dispatcher.UIThread.Invoke(_mainWindow.Close);
+ }
+}
\ No newline at end of file
diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml
index e0aae82..20258ff 100644
--- a/Views/MainWindow.axaml
+++ b/Views/MainWindow.axaml
@@ -6,15 +6,54 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="VaultSmpInstaller.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
- Icon="/Assets/avalonia-logo.ico"
- Title="VaultSmpInstaller">
+ Icon="/Assets/icon.ico"
+ Title="Vault SMP Installer"
+ CanResize="False">
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ You are running
+
+
+
+
+ SMP Vault Pack Installer
+
+
+ Optional Packs
+
+
+
+ Disabled
+
+
+
+
+
+
+
+
+ Enabled
+
+
+
+
+
+ Installed Packs
+
+
+
+
diff --git a/Views/MainWindow.axaml.cs b/Views/MainWindow.axaml.cs
index 333b6b5..c6d4318 100644
--- a/Views/MainWindow.axaml.cs
+++ b/Views/MainWindow.axaml.cs
@@ -1,11 +1,310 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using System.Threading;
+using System.Threading.Tasks;
using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.ReactiveUI;
+using Avalonia.Threading;
+using ReactiveUI;
+using VaultSmpInstaller.Data;
+using VaultSmpInstaller.ViewModels;
namespace VaultSmpInstaller.Views;
-public partial class MainWindow : Window
+public partial class MainWindow : ReactiveWindow
{
public MainWindow()
{
InitializeComponent();
+
+ this.WhenActivated(action => action(ViewModel!.ShowProfileSelectionDialog.RegisterHandler(DoShowDialogAsync)));
+
+ this.WhenActivated(_ => StartDownload());
+ }
+
+ private async Task ProcessInstance(String instancePath)
+ {
+ await using FileStream fs = new FileStream(Path.Combine(instancePath, "instance.json"), FileMode.Open);
+ ViewModel!.LatestInstanceConfig = await JsonSerializer.DeserializeAsync(fs, JsonContext.Default.InstanceConfig);
+ if (ViewModel!.LatestInstanceConfig != null)
+ {
+ ViewModel!.LatestInstanceConfig.InstancePath = instancePath;
+
+ foreach (var modProfile in ViewModel!.LatestInstanceConfig.ModProfiles.Values)
+ if (modProfile.Required || modProfile.DefaultState)
+ modProfile.IsEnabled = true;
+
+ ViewModel!.LatestVersion = ViewModel!.LatestInstanceConfig.Version;
+
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.LatestInstanceConfig));
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfiles));
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfiles));
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfileNames));
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfileNames));
+
+ ViewModel!.SelectProfileCommand.Execute(null);
+ }
+ else
+ {
+ throw new Exception("Failed to download new version");
+ }
+ }
+
+ private async Task DoShowDialogAsync(InteractionContext interaction)
+ {
+ var dialog = new ProfileWindow1();
+ dialog.DataContext = interaction.Input;
+
+ var result = await dialog.ShowDialog(this);
+ interaction.SetOutput(result);
+ }
+
+ private readonly DownloadingWindow _downloadingWindow = new DownloadingWindow();
+
+ private void StartDownload()
+ {
+ var downloadThread = new Thread(async (arg) =>
+ {
+ if (arg is CancellationTokenSource)
+ {
+ 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);
+
+ fs.Close();
+
+ ZipFile.ExtractToDirectory(archivePath, extractPath);
+
+ tokenSource.Token.ThrowIfCancellationRequested();
+
+ File.Delete(archivePath);
+
+ await Dispatcher.UIThread.Invoke(() => ProcessInstance(extractPath));
+ }
+ catch (OperationCanceledException e)
+ {
+ try
+ {
+ File.Delete(archivePath);
+ Directory.Delete(extractPath, true);
+ }
+ catch (Exception ignored)
+ {
+ // ignored
+ }
+ Dispatcher.UIThread.Invoke(Close);
+ }
+ Dispatcher.UIThread.Invoke(() => _downloadingWindow.Close());
+ }
+ });
+
+ CancellationTokenSource source = new CancellationTokenSource();
+ downloadThread.Start(source);
+ _downloadingWindow.DataContext = new DownloadingWindowViewModel();
+ _downloadingWindow.Closed += (_, _) => source.Cancel();
+ _downloadingWindow.ShowDialog(this);
+ }
+
+ private string _disabledSelected = "";
+
+ private void DisabledSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (e.AddedItems is [String])
+ _disabledSelected = (String)e.AddedItems[0]!;
+ }
+
+ private void EnableSelected(object? sender, RoutedEventArgs e)
+ {
+ if (ViewModel!.LatestInstanceConfig!.ModProfiles.ContainsKey(_disabledSelected))
+ {
+ ViewModel!.LatestInstanceConfig!.ModProfiles[_disabledSelected].IsEnabled = true;
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfiles));
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfileNames));
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfiles));
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfileNames));
+ _disabledSelected = "";
+ }
+ }
+
+ private string _enabledSelected = "";
+ private void EnabledSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (e.AddedItems is [String])
+ _enabledSelected = (String)e.AddedItems[0]!;
+ }
+
+ private void DisableSelected(object? sender, RoutedEventArgs e)
+ {
+ if (ViewModel!.LatestInstanceConfig!.ModProfiles.ContainsKey(_enabledSelected) && !ViewModel!.LatestInstanceConfig!.ModProfiles[_enabledSelected].Required)
+ {
+ ViewModel!.LatestInstanceConfig!.ModProfiles[_enabledSelected].IsEnabled = false;
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfiles));
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.EnabledModProfileNames));
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfiles));
+ ViewModel!.RaisePropertyChanged(nameof(ViewModel.DisabledModProfileNames));
+ _enabledSelected = "";
+ }
+ }
+
+ public ManualResetEvent ContinueInstalling = new ManualResetEvent(false);
+
+ private void InstallProfile(object? sender, RoutedEventArgs e)
+ {
+ Thread installThread = new Thread(() =>
+ {
+ var md5 = MD5.Create();
+ var viewModel = Dispatcher.UIThread.Invoke(() => ViewModel);
+ if (viewModel!.InstalledInstanceConfig == null)
+ {
+ viewModel.InstalledInstanceConfig = viewModel.LatestInstanceConfig!;
+ viewModel.InstalledVersion = viewModel.LatestInstanceConfig!.Version;
+ File.WriteAllText(Path.Combine(viewModel.SelectedInstance!.InstancePath, "installedInstance.json"),
+ JsonSerializer.Serialize(viewModel.LatestInstanceConfig, JsonContext.Default.InstanceConfig));
+
+ foreach (var (path, md5Hash) in viewModel.LatestInstanceConfig.ReplaceFiles)
+ {
+ var stream = File.ReadAllBytes(Path.Combine(viewModel.SelectedInstance.MinecraftPath, path));
+ if (BitConverter.ToString(md5.ComputeHash(stream)).Replace("-","").ToLower() != md5Hash)
+ {
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ var instanceNotIntactWindow = new InstanceNotIntactWindow(this);
+ instanceNotIntactWindow.DataContext = new ThemeViewModel();
+
+ instanceNotIntactWindow.ShowDialog(this);
+ });
+
+ ContinueInstalling.WaitOne();
+ break;
+ }
+ }
+
+ foreach (var path in viewModel.LatestInstanceConfig.ReplaceFiles.Keys)
+ {
+ var pathParts = path.Split(Path.DirectorySeparatorChar);
+ var currentPath = "";
+ if(!Directory.Exists(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles")))
+ {
+ Directory.CreateDirectory(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles"));
+ }
+ foreach (var part in pathParts[0..^1])
+ {
+ currentPath = Path.Combine(currentPath, part);
+ if(!Directory.Exists(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", currentPath)))
+ Directory.CreateDirectory(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", currentPath));
+ }
+ File.Copy(Path.Combine(viewModel.SelectedInstance.MinecraftPath, path), Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", path));
+ }
+ }
+
+ CopyAll(Path.Combine(viewModel.LatestInstanceConfig!.InstancePath, "config"), viewModel.SelectedInstance!.ConfigPath);
+ CopyAll(Path.Combine(viewModel.LatestInstanceConfig.InstancePath, "scripts"), viewModel.SelectedInstance.ScriptsPath);
+
+ if(!Directory.Exists(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles")))
+ {
+ Directory.CreateDirectory(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles"));
+ }
+ if(!Directory.Exists(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", "mods")))
+ {
+ Directory.CreateDirectory(Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", "mods"));
+ }
+
+ foreach (var (profileName, modProfile) in viewModel.LatestInstanceConfig.ModProfiles)
+ {
+ if (modProfile.IsEnabled)
+ {
+ foreach (var (modName, md5Hash) in modProfile.Replaces)
+ {
+ if (File.Exists(Path.Combine(viewModel.SelectedInstance.ModsPath, modName)))
+ {
+ var stream = File.ReadAllBytes(Path.Combine(viewModel.SelectedInstance.ModsPath, modName));
+ if (BitConverter.ToString(md5.ComputeHash(stream)).Replace("-","").ToLower() == md5Hash)
+ {
+ File.Copy(Path.Combine(viewModel.SelectedInstance.ModsPath, modName), Path.Combine(viewModel.SelectedInstance.InstancePath, "originalFiles", "mods", modName));
+ File.Delete(Path.Combine(viewModel.SelectedInstance.ModsPath, modName));
+ }
+ }
+ }
+
+ foreach (var modName in modProfile.Mods.Keys)
+ {
+ File.Copy(Path.Combine(viewModel.LatestInstanceConfig.InstancePath, "mods", modName), Path.Combine(viewModel.SelectedInstance.ModsPath, modName), true);
+ }
+ } else if (viewModel.InstalledInstanceConfig.ModProfiles[profileName].IsEnabled)
+ {
+ foreach (var modName in modProfile.Mods.Keys)
+ {
+ if(File.Exists(Path.Combine(viewModel.SelectedInstance.ModsPath, modName)))
+ File.Delete(Path.Combine(viewModel.SelectedInstance.ModsPath, modName));
+ }
+ }
+ }
+
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ var successWindow = new SuccessWindow(this);
+ successWindow.DataContext = new ThemeViewModel();
+
+ successWindow.ShowDialog(this);
+ });
+
+ viewModel.InstalledInstanceConfig = viewModel.LatestInstanceConfig!;
+ viewModel.InstalledVersion = viewModel.LatestInstanceConfig!.Version;
+
+ viewModel.RaisePropertyChanged(nameof(viewModel.InstalledInstanceConfig));
+ viewModel.RaisePropertyChanged(nameof(viewModel.InstalledModProfiles));
+ viewModel.RaisePropertyChanged(nameof(viewModel.InstalledModProfileNames));
+
+ ContinueInstalling.Reset();
+
+ ContinueInstalling.WaitOne();
+ });
+ installThread.Start();
+ }
+
+ public static void CopyAll(string source, string target)
+ {
+ CopyAll(new DirectoryInfo(source), new DirectoryInfo(target));
+ }
+ public static void CopyAll(DirectoryInfo source, DirectoryInfo target)
+ {
+ if (source.FullName.ToLower() == target.FullName.ToLower())
+ {
+ return;
+ }
+
+ // Check if the target directory exists, if not, create it.
+ if (Directory.Exists(target.FullName) == false)
+ {
+ Directory.CreateDirectory(target.FullName);
+ }
+
+ // Copy each file into it's new directory.
+ foreach (FileInfo fi in source.GetFiles())
+ {
+ fi.CopyTo(Path.Combine(target.ToString(), fi.Name), true);
+ }
+
+ // Copy each subdirectory using recursion.
+ foreach (DirectoryInfo diSourceSubDir in source.GetDirectories())
+ {
+ DirectoryInfo nextTargetSubDir =
+ target.CreateSubdirectory(diSourceSubDir.Name);
+ CopyAll(diSourceSubDir, nextTargetSubDir);
+ }
}
}
\ No newline at end of file
diff --git a/Views/ProfileWindow1.axaml b/Views/ProfileWindow1.axaml
new file mode 100644
index 0000000..81da0ca
--- /dev/null
+++ b/Views/ProfileWindow1.axaml
@@ -0,0 +1,19 @@
+
+
+ Please Select Prism Launcher or Curseforge
+
+
+
+
diff --git a/Views/ProfileWindow1.axaml.cs b/Views/ProfileWindow1.axaml.cs
new file mode 100644
index 0000000..1345833
--- /dev/null
+++ b/Views/ProfileWindow1.axaml.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.ReactiveUI;
+using ReactiveUI;
+using VaultSmpInstaller.ViewModels;
+
+namespace VaultSmpInstaller.Views;
+
+public partial class ProfileWindow1 : ReactiveWindow
+{
+ public ProfileWindow1()
+ {
+ InitializeComponent();
+
+ this.WhenActivated(d => d(ViewModel!.UseCurseforgeCommand.Subscribe(Close)));
+ this.WhenActivated(d => d(ViewModel!.UsePrismCommand.Subscribe(Close)));
+
+ this.WhenActivated(action =>
+ action(ViewModel!.ShowProfileSelectionDialog.RegisterHandler(DoShowDialogAsync)));
+ }
+ private async Task DoShowDialogAsync(InteractionContext interaction)
+ {
+ var dialog = new ProfileWindow2();
+ dialog.DataContext = interaction.Input;
+
+ var result = await dialog.ShowDialog(this);
+ interaction.SetOutput(result);
+ }
+
+}
\ No newline at end of file
diff --git a/Views/ProfileWindow2.axaml b/Views/ProfileWindow2.axaml
new file mode 100644
index 0000000..629c518
--- /dev/null
+++ b/Views/ProfileWindow2.axaml
@@ -0,0 +1,21 @@
+
+
+
+ Select VH3 Instance
+
+
+
+
+
diff --git a/Views/ProfileWindow2.axaml.cs b/Views/ProfileWindow2.axaml.cs
new file mode 100644
index 0000000..064a953
--- /dev/null
+++ b/Views/ProfileWindow2.axaml.cs
@@ -0,0 +1,26 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using ReactiveUI;
+using VaultSmpInstaller.ViewModels;
+
+namespace VaultSmpInstaller.Views;
+
+public partial class ProfileWindow2 : ReactiveWindow
+{
+ public ProfileWindow2()
+ {
+ InitializeComponent();
+
+ this.WhenActivated(action => action(ViewModel!.SelectProfileCommand.Subscribe(Close)));
+ }
+
+ private void SelectingItemsControl_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ ViewModel!.SelectedInstance = ViewModel.Instances[((string)e.AddedItems[0]!)!];
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.IsInstanceSelected));
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.SelectedInstance));
+ }
+}
\ No newline at end of file
diff --git a/Views/SuccessWindow.axaml b/Views/SuccessWindow.axaml
new file mode 100644
index 0000000..f191728
--- /dev/null
+++ b/Views/SuccessWindow.axaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Installation Successful!
+
+
+
diff --git a/Views/SuccessWindow.axaml.cs b/Views/SuccessWindow.axaml.cs
new file mode 100644
index 0000000..37cd042
--- /dev/null
+++ b/Views/SuccessWindow.axaml.cs
@@ -0,0 +1,23 @@
+using Avalonia;
+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(MainWindow mainWindow)
+ {
+ InitializeComponent();
+ this._mainWindow = mainWindow;
+ }
+
+ private void Ok(object? sender, RoutedEventArgs e)
+ {
+ _mainWindow.ContinueInstalling.Set();
+ Dispatcher.UIThread.Invoke(Close);
+ }
+}
\ No newline at end of file