Initial (very large) commit
This commit is contained in:
parent
0fed23192e
commit
43b9ea727e
21
App.axaml
21
App.axaml
@ -1,15 +1,24 @@
|
|||||||
<Application xmlns="https://github.com/avaloniaui"
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
x:Class="VaultSmpInstaller.App"
|
x:Class="VaultSmpInstaller.App"
|
||||||
|
x:DataType="viewModels:ThemeViewModel"
|
||||||
xmlns:local="using:VaultSmpInstaller"
|
xmlns:local="using:VaultSmpInstaller"
|
||||||
RequestedThemeVariant="Default">
|
xmlns:viewModels="clr-namespace:VaultSmpInstaller.ViewModels"
|
||||||
|
RequestedThemeVariant="Dark">
|
||||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||||
|
|
||||||
<Application.DataTemplates>
|
|
||||||
<local:ViewLocator/>
|
|
||||||
</Application.DataTemplates>
|
|
||||||
|
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<FluentTheme />
|
<FluentTheme />
|
||||||
|
<StyleInclude Source="avares://FluentAvalonia.ProgressRing/Styling/Controls/ProgressRing.axaml" />
|
||||||
|
<Style Selector="Button, TextBlock, ListBoxItem">
|
||||||
|
<Setter Property="Button.Foreground" Value="{Binding TextColor}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock, Button">
|
||||||
|
<Setter Property="Button.HorizontalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button">
|
||||||
|
<Setter Property="Button.Background" Value="{Binding ButtonBackground}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
</Application>
|
</Application>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 172 KiB |
BIN
Assets/icon.ico
Normal file
BIN
Assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
17
Data/InstanceConfig.cs
Normal file
17
Data/InstanceConfig.cs
Normal file
@ -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<string, ModProfile> ModProfiles { get; set; }
|
||||||
|
[JsonPropertyName("replaceFiles")]
|
||||||
|
public Dictionary<string, string> ReplaceFiles { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string InstancePath { get; set; }
|
||||||
|
}
|
||||||
16
Data/JsonContext.cs
Normal file
16
Data/JsonContext.cs
Normal file
@ -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<string, string>))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, ModProfile>))]
|
||||||
|
public partial class JsonContext: JsonSerializerContext
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
22
Data/ModProfile.cs
Normal file
22
Data/ModProfile.cs
Normal file
@ -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<string, string> Mods { get; set; }
|
||||||
|
[JsonPropertyName("replaces")]
|
||||||
|
public Dictionary<string, string> Replaces { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("enabled")]
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
}
|
||||||
@ -6,7 +6,14 @@
|
|||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
<!-- <TrimMode>copyused</TrimMode>-->
|
||||||
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
|
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<TrimmerRootAssembly Include="VaultSmpInstaller" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Models\"/>
|
<Folder Include="Models\"/>
|
||||||
@ -22,5 +29,6 @@
|
|||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.6"/>
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.6"/>
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.6"/>
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.6"/>
|
||||||
|
<PackageReference Include="FluentAvalonia.ProgressRing" Version="1.69.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
ViewModels/DownloadingWindowViewModel.cs
Normal file
11
ViewModels/DownloadingWindowViewModel.cs
Normal file
@ -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");
|
||||||
|
}
|
||||||
@ -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
|
public class MainWindowViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
#pragma warning disable CA1822 // Mark members as static
|
public static Brush Background => SolidColorBrush.Parse("#282A36");
|
||||||
public string Greeting => "Welcome to Avalonia!";
|
public static Brush SecondaryBackground => SolidColorBrush.Parse("#44475A");
|
||||||
#pragma warning restore CA1822 // Mark members as static
|
public static Brush ButtonBackground => SolidColorBrush.Parse("#6272A4");
|
||||||
|
public static Brush TextColor => SolidColorBrush.Parse("#F8F8F2");
|
||||||
|
|
||||||
|
public Interaction<ProfileWindow1ViewModel, ProfileWindow2ViewModel.InstanceInfo?> ShowProfileSelectionDialog { get; }
|
||||||
|
|
||||||
|
public ICommand SelectProfileCommand { get; }
|
||||||
|
|
||||||
|
public MainWindowViewModel()
|
||||||
|
{
|
||||||
|
ShowProfileSelectionDialog = new Interaction<ProfileWindow1ViewModel, ProfileWindow2ViewModel.InstanceInfo?>();
|
||||||
|
|
||||||
|
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<InstanceConfig>(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<string, ModProfile> EnabledModProfiles => LatestInstanceConfig?.ModProfiles.Where(pair => pair.Value.IsEnabled).ToDictionary() ?? new Dictionary<string, ModProfile>();
|
||||||
|
public List<string> EnabledModProfileNames => EnabledModProfiles.Keys.ToList();
|
||||||
|
public Dictionary<string, ModProfile> DisabledModProfiles => LatestInstanceConfig?.ModProfiles.Where(pair => !pair.Value.IsEnabled).ToDictionary() ?? new Dictionary<string, ModProfile>();
|
||||||
|
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 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;
|
||||||
}
|
}
|
||||||
126
ViewModels/ProfileWindow1ViewModel.cs
Normal file
126
ViewModels/ProfileWindow1ViewModel.cs
Normal file
@ -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<ProfileWindow2ViewModel, ProfileWindow2ViewModel.InstanceInfo?> ShowProfileSelectionDialog { get; }
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, ProfileWindow2ViewModel.InstanceInfo?> UseCurseforgeCommand { get; }
|
||||||
|
public ReactiveCommand<Unit, ProfileWindow2ViewModel.InstanceInfo?> UsePrismCommand { get; }
|
||||||
|
|
||||||
|
public ProfileWindow1ViewModel()
|
||||||
|
{
|
||||||
|
var curseforgeDir = CurseforgeInstanceDir;
|
||||||
|
var prismDir = PrismInstanceDir;
|
||||||
|
|
||||||
|
ShowProfileSelectionDialog = new Interaction<ProfileWindow2ViewModel, ProfileWindow2ViewModel.InstanceInfo?>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
95
ViewModels/ProfileWindow2ViewModel.cs
Normal file
95
ViewModels/ProfileWindow2ViewModel.cs
Normal file
@ -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<ProfileWindow2ViewModel, ProfileWindow2ViewModel.InstanceInfo?> ShowProfileSelectionDialog { get; }
|
||||||
|
public ReactiveCommand<Unit, ProfileWindow2ViewModel.InstanceInfo?> 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<string, InstanceInfo> Instances { get; }
|
||||||
|
public List<string> InstanceNames { get; }
|
||||||
|
|
||||||
|
public InstanceInfo? SelectedInstance { get; set; } = null;
|
||||||
|
|
||||||
|
public Boolean IsInstanceSelected => SelectedInstance != null;
|
||||||
|
|
||||||
|
public ProfileWindow2ViewModel(InstanceType launcherType, string instancesDir)
|
||||||
|
{
|
||||||
|
Instances = new Dictionary<string, InstanceInfo>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
11
ViewModels/ThemeViewModel.cs
Normal file
11
ViewModels/ThemeViewModel.cs
Normal file
@ -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");
|
||||||
|
}
|
||||||
23
Views/DownloadingWindow.axaml
Normal file
23
Views/DownloadingWindow.axaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
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.ProgressRing"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="160"
|
||||||
|
x:Class="VaultSmpInstaller.Views.DownloadingWindow"
|
||||||
|
x:DataType="vm:DownloadingWindowViewModel"
|
||||||
|
Icon="/Assets/icon.ico"
|
||||||
|
Title="Downloading Update"
|
||||||
|
Width="300" Height="160"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
CanResize="False">
|
||||||
|
<Design.DataContext>
|
||||||
|
<vm:DownloadingWindowViewModel/>
|
||||||
|
</Design.DataContext>
|
||||||
|
<StackPanel Background="{Binding Background}">
|
||||||
|
<TextBlock FontSize="18" Margin="10, 10, 10, 0">Downloading Latest Release</TextBlock>
|
||||||
|
<TextBlock FontSize="10" Margin="0, 0, 0, 20">This popup will close when complete</TextBlock>
|
||||||
|
<controls:ProgressRing IsIndeterminate="True" BorderThickness="10" Width="80" Height="80" />
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
||||||
13
Views/DownloadingWindow.axaml.cs
Normal file
13
Views/DownloadingWindow.axaml.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace VaultSmpInstaller.Views;
|
||||||
|
|
||||||
|
public partial class DownloadingWindow : Window
|
||||||
|
{
|
||||||
|
public DownloadingWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Views/InstanceNotIntactWindow.axaml
Normal file
27
Views/InstanceNotIntactWindow.axaml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:VaultSmpInstaller.ViewModels"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400"
|
||||||
|
x:Class="VaultSmpInstaller.Views.InstanceNotIntactWindow"
|
||||||
|
x:DataType="vm:ThemeViewModel"
|
||||||
|
Icon="/Assets/icon.ico"
|
||||||
|
Title="Instance not Intact"
|
||||||
|
Width="600" Height="400"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
CanResize="False">
|
||||||
|
<Design.DataContext>
|
||||||
|
<vm:ThemeViewModel/>
|
||||||
|
</Design.DataContext>
|
||||||
|
<StackPanel Background="{Binding Background}">
|
||||||
|
<TextBlock>This instance has been modified outside of this script.</TextBlock>
|
||||||
|
<TextBlock>If you have used the previous script this should be fine.</TextBlock>
|
||||||
|
<TextBlock>Otherwise I recommend making a new instance.</TextBlock>
|
||||||
|
<TextBlock>From this point, beware of issues ahead.</TextBlock>
|
||||||
|
<Grid ColumnDefinitions="*, *">
|
||||||
|
<Button Grid.Column="0" Margin="10, 10, 5, 10" Click="Cancel">Cancel</Button>
|
||||||
|
<Button Grid.Column="1" Margin="5, 10, 10, 10" Click="Continue">Continue</Button>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
||||||
29
Views/InstanceNotIntactWindow.axaml.cs
Normal file
29
Views/InstanceNotIntactWindow.axaml.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,15 +6,54 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="VaultSmpInstaller.Views.MainWindow"
|
x:Class="VaultSmpInstaller.Views.MainWindow"
|
||||||
x:DataType="vm:MainWindowViewModel"
|
x:DataType="vm:MainWindowViewModel"
|
||||||
Icon="/Assets/avalonia-logo.ico"
|
Icon="/Assets/icon.ico"
|
||||||
Title="VaultSmpInstaller">
|
Title="Vault SMP Installer"
|
||||||
|
CanResize="False">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<!-- This only sets the DataContext for the previewer in an IDE,
|
|
||||||
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
|
|
||||||
<vm:MainWindowViewModel/>
|
<vm:MainWindowViewModel/>
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
<Grid Background="{Binding Background}" RowDefinitions="Auto, Auto, *">
|
||||||
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
<Grid Grid.Row="0" Background="{Binding SecondaryBackground}" MaxHeight="100" Margin="10" ColumnDefinitions="Auto,2*,2*,Auto">
|
||||||
|
<StackPanel VerticalAlignment="Center" Grid.Column="0">
|
||||||
|
<Button Margin="5" Command="{Binding SelectProfileCommand}">Select a Profile</Button>
|
||||||
|
<TextBlock Margin="5, 0, 5, 5" FontSize="10" Text="{Binding SelectedInstanceName}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Right" Margin="25, 0" Grid.Column="1">
|
||||||
|
<TextBlock Text="{Binding InstalledVersion, StringFormat=Installed Version: {0}}"/>
|
||||||
|
<TextBlock Text="{Binding LatestVersion, StringFormat=Latest Version: {0}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Left" Margin="25, 0" Grid.Column="2">
|
||||||
|
<TextBlock>You are running</TextBlock>
|
||||||
|
<TextBlock Text="{Binding IsLatestVersionText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Margin="5" Grid.Column="3" Click="InstallProfile" IsEnabled="{Binding IsInstanceSelected}">Install to Profile</Button>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Grid.Row="1" Margin="15" FontSize="24">SMP Vault Pack Installer</TextBlock>
|
||||||
|
<Grid Grid.Row="2" ColumnDefinitions="*,*">
|
||||||
|
<Grid Grid.Column="0" Margin="10" Background="{Binding SecondaryBackground}" RowDefinitions="Auto,*">
|
||||||
|
<TextBlock Margin="15" FontSize="18">Optional Packs</TextBlock>
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="*, Auto, *">
|
||||||
|
<Grid Grid.Column="0" RowDefinitions="Auto, *">
|
||||||
|
<Rectangle Grid.Row="0" Margin="5, 0" Height="18" Fill="{Binding Background}" RadiusX="4" RadiusY="4"/>
|
||||||
|
<TextBlock Grid.Row="0">Disabled</TextBlock>
|
||||||
|
<ListBox Grid.Row="1" SelectionChanged="DisabledSelectionChanged" ItemsSource="{Binding DisabledModProfileNames}" Background="{Binding Background}" Margin="5" />
|
||||||
|
</Grid>
|
||||||
|
<Grid Grid.Column="1" RowDefinitions="*, Auto, 10, Auto, *">
|
||||||
|
<Button Grid.Row="1" Click="EnableSelected">→</Button>
|
||||||
|
<Button Grid.Row="3" Click="DisableSelected">←</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid Grid.Column="2" RowDefinitions="Auto, *">
|
||||||
|
<Rectangle Grid.Row="0" Margin="5, 0" Height="18" Fill="{Binding Background}" RadiusX="4" RadiusY="4"/>
|
||||||
|
<TextBlock Grid.Row="0">Enabled</TextBlock>
|
||||||
|
<ListBox Grid.Row="1" SelectionChanged="EnabledSelectionChanged" ItemsSource="{Binding EnabledModProfileNames}" Background="{Binding Background}" Margin="5" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid Grid.Column="1" Margin="10" Background="{Binding SecondaryBackground}" RowDefinitions="Auto,*">
|
||||||
|
<TextBlock Margin="15" FontSize="18">Installed Packs</TextBlock>
|
||||||
|
<ListBox ItemsSource="{Binding InstalledModProfileNames}" Background="{Binding Background}" Grid.Row="1" Margin="5" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@ -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.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using ReactiveUI;
|
||||||
|
using VaultSmpInstaller.Data;
|
||||||
|
using VaultSmpInstaller.ViewModels;
|
||||||
|
|
||||||
namespace VaultSmpInstaller.Views;
|
namespace VaultSmpInstaller.Views;
|
||||||
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
|
||||||
{
|
{
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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<InstanceConfig>(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<ProfileWindow1ViewModel, ProfileWindow2ViewModel.InstanceInfo?> interaction)
|
||||||
|
{
|
||||||
|
var dialog = new ProfileWindow1();
|
||||||
|
dialog.DataContext = interaction.Input;
|
||||||
|
|
||||||
|
var result = await dialog.ShowDialog<ProfileWindow2ViewModel.InstanceInfo?>(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
19
Views/ProfileWindow1.axaml
Normal file
19
Views/ProfileWindow1.axaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:VaultSmpInstaller.ViewModels"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="600"
|
||||||
|
x:Class="VaultSmpInstaller.Views.ProfileWindow1"
|
||||||
|
x:DataType="vm:ProfileWindow1ViewModel"
|
||||||
|
Icon="/Assets/icon.ico"
|
||||||
|
Title="Select a Launcher"
|
||||||
|
Width="400" Height="175"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
CanResize="False">
|
||||||
|
<Grid Background="{Binding Background}" RowDefinitions="Auto, *, *">
|
||||||
|
<TextBlock Grid.Row="0" FontSize="18" Margin="0, 10">Please Select Prism Launcher or Curseforge</TextBlock>
|
||||||
|
<Button Grid.Row="1" Command="{Binding UsePrismCommand}" FontSize="18" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Margin="25, 5" Content="{Binding PrismButtonText}" IsEnabled="{Binding IsPrismInstalled}"/>
|
||||||
|
<Button Grid.Row="2" Command="{Binding UseCurseforgeCommand}" FontSize="18" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Margin="25, 5" Content="{Binding CurseforgeButtonText}" IsEnabled="{Binding IsCurseforgeInstalled}"/>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
30
Views/ProfileWindow1.axaml.cs
Normal file
30
Views/ProfileWindow1.axaml.cs
Normal file
@ -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<ProfileWindow1ViewModel>
|
||||||
|
{
|
||||||
|
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<ProfileWindow2ViewModel, ProfileWindow2ViewModel.InstanceInfo?> interaction)
|
||||||
|
{
|
||||||
|
var dialog = new ProfileWindow2();
|
||||||
|
dialog.DataContext = interaction.Input;
|
||||||
|
|
||||||
|
var result = await dialog.ShowDialog<ProfileWindow2ViewModel.InstanceInfo?>(this);
|
||||||
|
interaction.SetOutput(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
Views/ProfileWindow2.axaml
Normal file
21
Views/ProfileWindow2.axaml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:VaultSmpInstaller.ViewModels"
|
||||||
|
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="VaultSmpInstaller.Views.ProfileWindow2"
|
||||||
|
x:DataType="vm:ProfileWindow2ViewModel"
|
||||||
|
Icon="/Assets/icon.ico"
|
||||||
|
Title="Select a Profile"
|
||||||
|
Width="400" Height="600"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
CanResize="False">
|
||||||
|
<DockPanel Background="{Binding Background}">
|
||||||
|
<Grid Background="{Binding SecondaryBackground}" Margin="5" RowDefinitions="Auto, *, Auto">
|
||||||
|
<TextBlock Grid.Row="0" FontSize="18" Margin="0, 10">Select VH3 Instance</TextBlock>
|
||||||
|
<ListBox Grid.Row="1" SelectionChanged="SelectingItemsControl_OnSelectionChanged" Margin="10, 5" ItemsSource="{Binding InstanceNames}" />
|
||||||
|
<Button Grid.Row="2" IsEnabled="{Binding IsInstanceSelected}" Command="{Binding SelectProfileCommand}" FontSize="18" Margin="10, 5" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center">Continue</Button>
|
||||||
|
</Grid>
|
||||||
|
</DockPanel>
|
||||||
|
</Window>
|
||||||
26
Views/ProfileWindow2.axaml.cs
Normal file
26
Views/ProfileWindow2.axaml.cs
Normal file
@ -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<ProfileWindow2ViewModel>
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Views/SuccessWindow.axaml
Normal file
21
Views/SuccessWindow.axaml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:VaultSmpInstaller.ViewModels"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="250" d:DesignHeight="150"
|
||||||
|
x:Class="VaultSmpInstaller.Views.SuccessWindow"
|
||||||
|
x:DataType="vm:ThemeViewModel"
|
||||||
|
Icon="/Assets/icon.ico"
|
||||||
|
Title="Installation Successful"
|
||||||
|
Width="275" Height="100"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
CanResize="False">
|
||||||
|
<Design.DataContext>
|
||||||
|
<vm:ThemeViewModel/>
|
||||||
|
</Design.DataContext>
|
||||||
|
<Grid Background="{Binding Background}" RowDefinitions="Auto, *">
|
||||||
|
<TextBlock Grid.Row="0" FontSize="24" Margin="10">Installation Successful!</TextBlock>
|
||||||
|
<Button Grid.Row="1" Margin="10, 10, 5, 10" Click="Ok">Ok</Button>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
23
Views/SuccessWindow.axaml.cs
Normal file
23
Views/SuccessWindow.axaml.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user