Initial (very large) commit

This commit is contained in:
Connor Drahoss 2024-02-19 19:44:36 +01:00
parent 0fed23192e
commit 43b9ea727e
25 changed files with 1006 additions and 50 deletions

View File

@ -1,15 +1,24 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="VaultSmpInstaller.App"
x:DataType="viewModels:ThemeViewModel"
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. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

BIN
Assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

17
Data/InstanceConfig.cs Normal file
View 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
View 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
View 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; }
}

View File

@ -6,7 +6,14 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<!-- <TrimMode>copyused</TrimMode>-->
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<TrimmerRootAssembly Include="VaultSmpInstaller" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\"/>
@ -22,5 +29,6 @@
<!--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 Include="Avalonia.ReactiveUI" Version="11.0.6"/>
<PackageReference Include="FluentAvalonia.ProgressRing" Version="1.69.2" />
</ItemGroup>
</Project>

View File

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

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

View File

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

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

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

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

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

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

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

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

View File

@ -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">
<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/>
</Design.DataContext>
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Grid Background="{Binding Background}" RowDefinitions="Auto, Auto, *">
<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>

View File

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

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

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

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

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

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