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/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