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