|
@@ -19,7 +19,7 @@ using System.Linq.Expressions;
|
|
|
|
|
|
namespace VeloeMinecraftLauncher.ViewModels;
|
|
|
|
|
|
-public class VersionsDownloaderViewModel : ViewModelBase
|
|
|
+public class VersionsDownloaderViewModel : ViewModelBase, IDisposable
|
|
|
{
|
|
|
private string _downloadButtonText = "Download";
|
|
|
private bool _showOld = false;
|
|
@@ -65,6 +65,10 @@ public class VersionsDownloaderViewModel : ViewModelBase
|
|
|
public VersionsDownloaderViewModel()
|
|
|
{
|
|
|
IsControlsEnabled = false;
|
|
|
+
|
|
|
+ this.PropertyChanged += OnFilteredVersionPropertyChanged;
|
|
|
+ this.PropertyChanged += OnDownloadedVersionPropertyChanged;
|
|
|
+
|
|
|
try
|
|
|
{
|
|
|
_logger = Settings.logger;
|
|
@@ -105,91 +109,13 @@ public class VersionsDownloaderViewModel : ViewModelBase
|
|
|
public Entity.VersionManifest.Version FilteredVersion
|
|
|
{
|
|
|
get { return _filteredVersion; }
|
|
|
- set {
|
|
|
- this.RaiseAndSetIfChanged(ref _filteredVersion, value);
|
|
|
-
|
|
|
+ set
|
|
|
+ {
|
|
|
InstallFabric = false;
|
|
|
InstallForge = false;
|
|
|
InstallOptifine = false;
|
|
|
InstallForgeOptifine = false;
|
|
|
-
|
|
|
- if (value is null)
|
|
|
- return;
|
|
|
-
|
|
|
- if (value.Type == "modpack")
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- if (System.IO.File.Exists(Settings.minecraftForlderPath + $"versions/{value.Id}/revision.json"))
|
|
|
- {
|
|
|
- if (value.ComplianceLevel > JsonSerializer.Deserialize<int>(System.IO.File.ReadAllText(Settings.minecraftForlderPath + $"versions/{value.Id}/revision.json")))
|
|
|
- DownloadButtonText = "Update Modpack";
|
|
|
- else
|
|
|
- DownloadButtonText = "Reinstall Modpack";
|
|
|
- }
|
|
|
- else
|
|
|
- if (System.IO.Directory.Exists($"{Settings.minecraftForlderPath}versions/{value.Id}") && System.IO.File.Exists($"{Settings.minecraftForlderPath}versions/{value.Id}/{value.Id}.json"))
|
|
|
- DownloadButtonText = "Update Modpack";
|
|
|
- else
|
|
|
- DownloadButtonText = "Download Modpack";
|
|
|
- }
|
|
|
- catch (Exception)
|
|
|
- {
|
|
|
- DownloadButtonText = "Update Modpack";
|
|
|
- }
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- if (System.IO.File.Exists(Settings.minecraftForlderPath + $"versions/{value.Id}/{value.Id}.json"))
|
|
|
- DownloadButtonText = "Reinstall";
|
|
|
- else
|
|
|
- DownloadButtonText = "Download";
|
|
|
- }
|
|
|
-
|
|
|
- try
|
|
|
- {
|
|
|
- Task.Run(() =>
|
|
|
- {
|
|
|
- _filteredVersionTokenSource.Cancel();
|
|
|
- _filteredVersionTokenSource.Dispose();
|
|
|
- _filteredVersionTokenSource = new();
|
|
|
- try
|
|
|
- {
|
|
|
- if (Downloader.IsFileAvaliable(@$"https://files.veloe.link/launcher/forge/Forge{value.Id}/Forge{value.Id}.json", _filteredVersionTokenSource.Token).Result)
|
|
|
- {
|
|
|
- if (Downloader.IsFileAvaliable(@$"https://files.veloe.link/launcher/forge/Forge{value.Id}/Optifine{value.Id}.jar", _filteredVersionTokenSource.Token).Result)
|
|
|
- InstallForgeOptifineVisible = true;
|
|
|
- InstallForgeVisible = true;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- InstallForgeVisible = false;
|
|
|
- InstallForgeOptifineVisible = false;
|
|
|
- }
|
|
|
-
|
|
|
- if (Downloader.IsFileAvaliable(@$"https://files.veloe.link/launcher/fabric/Fabric{value.Id}/Fabric{value.Id}.json", _filteredVersionTokenSource.Token).Result)
|
|
|
- InstallFabricVisible = true;
|
|
|
- else
|
|
|
- InstallFabricVisible = false;
|
|
|
-
|
|
|
- if (Downloader.IsFileAvaliable(@$"https://files.veloe.link/launcher/optifine/Optifine{value.Id}/Optifine{value.Id}.json", _filteredVersionTokenSource.Token).Result)
|
|
|
- InstallOptifineVisible = true;
|
|
|
- else
|
|
|
- InstallOptifineVisible = false;
|
|
|
- }
|
|
|
- catch (OperationCanceledException)
|
|
|
- {
|
|
|
- InstallForgeVisible = false;
|
|
|
- InstallForgeOptifineVisible = false;
|
|
|
- InstallFabricVisible = false;
|
|
|
- InstallOptifineVisible = false;
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- catch (Exception ex)
|
|
|
- {
|
|
|
- OpenErrorWindow(ex);
|
|
|
- }
|
|
|
+ this.RaiseAndSetIfChanged(ref _filteredVersion, value);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -199,330 +125,9 @@ public class VersionsDownloaderViewModel : ViewModelBase
|
|
|
set
|
|
|
{
|
|
|
this.RaiseAndSetIfChanged(ref _downloadedVersion, value);
|
|
|
- Task.Run(() =>
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- IsControlsEnabled = false;
|
|
|
- DownloadedVersionTree.Clear();
|
|
|
- DownloadedVersionTree = new();
|
|
|
- this.RaisePropertyChanged(nameof(DownloadedVersionTree));
|
|
|
- DownloadedVersionsDictionary.Clear();
|
|
|
-
|
|
|
- if (value is null)
|
|
|
- {
|
|
|
- IsControlsEnabled = true;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- Entity.Version.Version? version = null;
|
|
|
-
|
|
|
- foreach (var dversion in DownloadedVersions)
|
|
|
- {
|
|
|
- string json;
|
|
|
- using (StreamReader reader = new StreamReader(dversion.path))
|
|
|
- {
|
|
|
- json = reader.ReadToEnd();
|
|
|
- }
|
|
|
-
|
|
|
- var versionObject = JsonSerializer.Deserialize<Entity.Version.Version>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
|
|
- DownloadedVersionsDictionary.Add(dversion.version, versionObject);
|
|
|
-
|
|
|
- if (dversion == _downloadedVersion)
|
|
|
- {
|
|
|
- version = versionObject;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (version is null)
|
|
|
- {
|
|
|
- OpenErrorWindow("Json file is invalid!");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- var versionDir = new FileInfo(value.path)?.Directory?.FullName;
|
|
|
-
|
|
|
- if (versionDir is null)
|
|
|
- {
|
|
|
- OpenErrorWindow("Version folder is invalid!");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- //get version
|
|
|
-
|
|
|
- DownloadedVersionTree.Add(new TreeNode() { Title = "Version: " + (version.InheritsFrom ?? version.Id) });
|
|
|
-
|
|
|
- //calc Size
|
|
|
-
|
|
|
- var allSize = 0L;
|
|
|
- var dirSize = 0L;
|
|
|
- var dirInfo = new DirectoryInfo(versionDir);
|
|
|
- var sizeTreeNode = new TreeNode();
|
|
|
- var dirTreeNode = new TreeNode();
|
|
|
-
|
|
|
- dirTreeNode.SubNode.AddRange(DirectoryToTreeNode(dirInfo,out dirSize));
|
|
|
- dirTreeNode.Title = "versions/" + dirInfo.Name + " (" + BytesToString(dirSize) + ")";
|
|
|
- sizeTreeNode.SubNode.Add(dirTreeNode);
|
|
|
- allSize += dirSize;
|
|
|
-
|
|
|
- if (version.InheritsFrom is null)
|
|
|
- {
|
|
|
- dirInfo = new DirectoryInfo(Settings.minecraftForlderPath);
|
|
|
- sizeTreeNode.SubNode.AddRange(
|
|
|
- DirectoryToTreeNode(
|
|
|
- dirInfo,
|
|
|
- out dirSize,
|
|
|
- (dir) => dir.Name == ".mixin.out" ||
|
|
|
- dir.Name == "assets" ||
|
|
|
- dir.Name == "javaruntime" ||
|
|
|
- dir.Name == "libraries" ||
|
|
|
- dir.Name == "logs" ||
|
|
|
- dir.Name == "versions",
|
|
|
- (file) => file.Name.Contains("Veloe") ||
|
|
|
- file.Name.Contains("log") ||
|
|
|
- file.Name.Contains("exe") ||
|
|
|
- file.Name == "launcher_profiles.json" ||
|
|
|
- file.Name == "libHarfBuzzSharp.dll" ||
|
|
|
- file.Name == "libSkiaSharp.dll" ||
|
|
|
- file.Name == "settings.json")
|
|
|
- );
|
|
|
- allSize += dirSize;
|
|
|
- }
|
|
|
-
|
|
|
- sizeTreeNode.Title = "Size: " + BytesToString(allSize);
|
|
|
- DownloadedVersionTree.Add(sizeTreeNode);
|
|
|
-
|
|
|
- //calc Worlds
|
|
|
-
|
|
|
- dirInfo = null;
|
|
|
-
|
|
|
- if (version.InheritsFrom is null && Directory.Exists(Settings.minecraftForlderPath + "saves"))
|
|
|
- dirInfo = new DirectoryInfo(Settings.minecraftForlderPath + "saves");
|
|
|
- else
|
|
|
- if (Directory.Exists(versionDir + "/saves"))
|
|
|
- dirInfo = new DirectoryInfo(versionDir + "/saves");
|
|
|
-
|
|
|
- if (dirInfo is not null)
|
|
|
- {
|
|
|
- var worldsTreeNode = new TreeNode() { Title = "Worlds: " + dirInfo.GetDirectories().Count() };
|
|
|
-
|
|
|
- foreach (var world in dirInfo.GetDirectories())
|
|
|
- { worldsTreeNode.SubNode.Add(new TreeNode() { Title = world.Name }); }
|
|
|
-
|
|
|
- DownloadedVersionTree.Add(worldsTreeNode);
|
|
|
- }
|
|
|
-
|
|
|
- //check modloader
|
|
|
-
|
|
|
- var modsTreeNode = new TreeNode();
|
|
|
-
|
|
|
- if (version.InheritsFrom is null)
|
|
|
- modsTreeNode.Title = "Modloader: No";
|
|
|
- else if (version.Libraries.Any(l => l.Downloads?.Artifact?.Url?.Contains("forge") ?? false))
|
|
|
- {
|
|
|
- modsTreeNode.Title = "Modloader: Forge";
|
|
|
- OpenErrorWindow("This version contains forge modloader, it installs some libraries that can't be displayed and deleted in current launcher versions manager.");
|
|
|
- }
|
|
|
- else if (version.Libraries.Any(l => l.Name.Contains("fabric")))
|
|
|
- modsTreeNode.Title = "Modloader: Fabric";
|
|
|
-
|
|
|
- //get mods list
|
|
|
-
|
|
|
- if (modsTreeNode.Title != "No" && Directory.Exists(versionDir + "/mods"))
|
|
|
- {
|
|
|
- dirInfo = new DirectoryInfo(versionDir + "/mods");
|
|
|
-
|
|
|
- modsTreeNode.Title += " (" + dirInfo.EnumerateFiles("*.jar", SearchOption.TopDirectoryOnly).Count() + ")";
|
|
|
- foreach (var mod in dirInfo.EnumerateFiles("*.jar", SearchOption.TopDirectoryOnly))
|
|
|
- { modsTreeNode.SubNode.Add(new TreeNode() { Title = mod.Name }); }
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
- DownloadedVersionTree.Add(modsTreeNode);
|
|
|
-
|
|
|
- //calc resourcepacks
|
|
|
-
|
|
|
- dirInfo = null;
|
|
|
-
|
|
|
- if (version.InheritsFrom is null && Directory.Exists(Settings.minecraftForlderPath + "resourcepacks"))
|
|
|
- dirInfo = new DirectoryInfo(Settings.minecraftForlderPath + "resourcepacks");
|
|
|
- else
|
|
|
- if (Directory.Exists(versionDir + "/resourcepacks"))
|
|
|
- dirInfo = new DirectoryInfo(versionDir + "/resourcepacks");
|
|
|
-
|
|
|
- if (dirInfo is not null)
|
|
|
- {
|
|
|
- var resourcepacksTreeNode = new TreeNode();
|
|
|
-
|
|
|
- resourcepacksTreeNode.Title = "Resoursepacks: " + (dirInfo?.GetDirectories()?.Count() + dirInfo?.GetFiles().Count() ?? 0).ToString();
|
|
|
- foreach (var resourcepack in dirInfo?.GetFileSystemInfos())
|
|
|
- { resourcepacksTreeNode.SubNode.Add(new TreeNode() { Title = resourcepack.Name }); }
|
|
|
-
|
|
|
- DownloadedVersionTree.Add(resourcepacksTreeNode);
|
|
|
- }
|
|
|
-
|
|
|
- //calc assets
|
|
|
-
|
|
|
- var assetsFolderName = version.Assets;
|
|
|
-
|
|
|
- if (version.Assets is null &&
|
|
|
- version.InheritsFrom is not null &&
|
|
|
- DownloadedVersionsDictionary.TryGetValue(version.InheritsFrom, out var outVersion) &&
|
|
|
- outVersion?.Assets is not null)
|
|
|
- {
|
|
|
- assetsFolderName = outVersion.Assets;
|
|
|
- }
|
|
|
-
|
|
|
- if (Directory.Exists(Settings.minecraftForlderPath + "assets/" + assetsFolderName))
|
|
|
- {
|
|
|
- dirInfo = new DirectoryInfo(Settings.minecraftForlderPath + "assets/" + assetsFolderName);
|
|
|
- allSize = 0;
|
|
|
- foreach (var file in dirInfo.EnumerateFiles("*", SearchOption.AllDirectories))
|
|
|
- { allSize += file.Length; }
|
|
|
-
|
|
|
- var assetsTreeNode = new TreeNode();
|
|
|
- var usedByVersionCount = 0;
|
|
|
-
|
|
|
- foreach (var dictionaryVersion in DownloadedVersionsDictionary)
|
|
|
- {
|
|
|
- if ((dictionaryVersion.Value?.Assets == assetsFolderName &&
|
|
|
- dictionaryVersion.Value?.InheritsFrom is null) ||
|
|
|
- (dictionaryVersion.Value?.InheritsFrom is not null &&
|
|
|
- DownloadedVersionsDictionary.TryGetValue(dictionaryVersion.Value.InheritsFrom, out outVersion) &&
|
|
|
- outVersion?.Assets == assetsFolderName))
|
|
|
- {
|
|
|
- usedByVersionCount++;
|
|
|
- assetsTreeNode.SubNode.Add(new TreeNode() { Title = dictionaryVersion.Key });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- assetsTreeNode.Title = "Assets: " + version.Assets + " (" + BytesToString(allSize) + ") (" + usedByVersionCount + ")";
|
|
|
- DownloadedVersionTree.Add(assetsTreeNode);
|
|
|
- }
|
|
|
-
|
|
|
- // calc libraries
|
|
|
-
|
|
|
- var usedLibraries = version.Libraries;
|
|
|
-
|
|
|
- var libTreeNode = new TreeNode();
|
|
|
- libTreeNode.Title = "Libraries";
|
|
|
-
|
|
|
- if (version.InheritsFrom is not null && DownloadedVersionsDictionary.TryGetValue(version.InheritsFrom, out outVersion) && outVersion is not null)
|
|
|
- {
|
|
|
- usedLibraries.AddRange(outVersion.Libraries);
|
|
|
- }
|
|
|
-
|
|
|
- usedLibraries =
|
|
|
- usedLibraries
|
|
|
- .Where(l =>
|
|
|
- File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + l.Downloads?.Artifact?.Path)) ||
|
|
|
- File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + l.Downloads?.Classifiers?.NativesLinux?.Path)) ||
|
|
|
- File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + l.Downloads?.Classifiers?.NativesWindows?.Path)) ||
|
|
|
- File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + l.Downloads?.Classifiers?.NativesWindows64?.Path)) ||
|
|
|
- File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + l.Downloads?.Classifiers?.NativesWindows32?.Path)) ||
|
|
|
- File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + StartCommandBuilder.GetLibPathFromName(l.Name, true)))
|
|
|
- )
|
|
|
- .ToList();
|
|
|
-
|
|
|
- var libraries = GetLibrariesFromVersions(DownloadedVersionsDictionary)
|
|
|
- .Where(v => usedLibraries.Any(u => v.Value.Name == u.Name))
|
|
|
- .GroupBy(a => a.Value.Name)
|
|
|
- .Select(g =>
|
|
|
- {
|
|
|
- return new {
|
|
|
- Name = g.Key,
|
|
|
- Count = g.Select(a => a.Key).Distinct().Count(),
|
|
|
- Versions = g.Select(a => a.Key).Distinct(),
|
|
|
- LibraryUniqueInstances = g.Select(a => a.Value).Distinct() //IEnumerable cause there can be several lib blocks with different rules in one version json
|
|
|
- };
|
|
|
- });
|
|
|
-
|
|
|
- foreach (var lib in libraries)
|
|
|
- {
|
|
|
- var subLibTreeNode = new TreeNode()
|
|
|
- {
|
|
|
- Title = lib.Name + " (" + lib.Count + ")",
|
|
|
- };
|
|
|
-
|
|
|
- //if only one version uses it (selected version), then add path to lib in Tag for deletion
|
|
|
- if (lib.Count == 1)
|
|
|
- {
|
|
|
- subLibTreeNode.Tag = lib.LibraryUniqueInstances.Where(l => l.Downloads?.Artifact?.Path is not null).Select(l => l.Downloads?.Artifact?.Path).FirstOrDefault() ?? string.Empty;
|
|
|
- //if fabric library
|
|
|
- if (File.Exists(Settings.minecraftForlderPath + "libraries/" + StartCommandBuilder.GetLibPathFromName(lib.Name,true)))
|
|
|
- subLibTreeNode.Tag = StartCommandBuilder.GetLibPathFromName(lib.Name,true);
|
|
|
- }
|
|
|
-
|
|
|
- var subLibUsedVersionsTreeNodeHeader = new TreeNode() { Title = "Using in versions:" };
|
|
|
-
|
|
|
- foreach (var ver in lib.Versions)
|
|
|
- subLibUsedVersionsTreeNodeHeader.SubNode.Add(new TreeNode() { Title = ver });
|
|
|
-
|
|
|
- if (subLibUsedVersionsTreeNodeHeader.SubNode.Count > 0)
|
|
|
- subLibTreeNode.SubNode.Add(subLibUsedVersionsTreeNodeHeader);
|
|
|
-
|
|
|
- if (lib.LibraryUniqueInstances.Any(l=>l.Natives is not null) || lib.LibraryUniqueInstances.Any(l=>l.Downloads?.Classifiers is not null))
|
|
|
- {
|
|
|
- var subLibNativesTreeNodeHeader = new TreeNode() { Title = "Natives:" };
|
|
|
-
|
|
|
- //check classifiers
|
|
|
- var libraryNativeInstance = lib.LibraryUniqueInstances.Where(l => l.Natives is not null || l.Downloads?.Classifiers is not null).First();
|
|
|
-
|
|
|
- if (libraryNativeInstance.Downloads?.Classifiers?.NativesWindows is not null)
|
|
|
- {
|
|
|
- subLibNativesTreeNodeHeader.SubNode.Add(new TreeNode()
|
|
|
- {
|
|
|
- Title = Path.GetFileName(libraryNativeInstance.Downloads?.Classifiers?.NativesWindows.Path) ?? "No lib name",
|
|
|
- Tag = lib.Count == 1 ? libraryNativeInstance.Downloads?.Classifiers?.NativesWindows?.Path ?? string.Empty : string.Empty
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- if (libraryNativeInstance.Downloads?.Classifiers?.NativesWindows32 is not null)
|
|
|
- {
|
|
|
- subLibNativesTreeNodeHeader.SubNode.Add(new TreeNode()
|
|
|
- {
|
|
|
- Title = Path.GetFileName(libraryNativeInstance.Downloads?.Classifiers?.NativesWindows32.Path) ?? "No lib name",
|
|
|
- Tag = lib.Count == 1 ? libraryNativeInstance.Downloads?.Classifiers?.NativesWindows32?.Path ?? string.Empty : string.Empty
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- if (libraryNativeInstance.Downloads?.Classifiers?.NativesWindows64 is not null)
|
|
|
- {
|
|
|
- subLibNativesTreeNodeHeader.SubNode.Add(new TreeNode()
|
|
|
- {
|
|
|
- Title = Path.GetFileName(libraryNativeInstance.Downloads?.Classifiers?.NativesWindows64.Path) ?? "No lib name",
|
|
|
- Tag = lib.Count == 1 ? libraryNativeInstance.Downloads?.Classifiers?.NativesWindows64?.Path ?? string.Empty : string.Empty
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- if (libraryNativeInstance.Downloads?.Classifiers?.NativesLinux is not null)
|
|
|
- {
|
|
|
- subLibNativesTreeNodeHeader.SubNode.Add(new TreeNode()
|
|
|
- {
|
|
|
- Title = Path.GetFileName(libraryNativeInstance.Downloads?.Classifiers?.NativesLinux.Path) ?? "No lib name",
|
|
|
- Tag = lib.Count == 1 ? libraryNativeInstance.Downloads?.Classifiers?.NativesLinux?.Path ?? string.Empty : string.Empty
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- if (subLibNativesTreeNodeHeader.SubNode.Count > 0)
|
|
|
- subLibTreeNode.SubNode.Add(subLibNativesTreeNodeHeader);
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
- libTreeNode.SubNode.Add(subLibTreeNode);
|
|
|
- }
|
|
|
-
|
|
|
- _libraries = libTreeNode;
|
|
|
- DownloadedVersionTree.Add(libTreeNode);
|
|
|
- }
|
|
|
- finally
|
|
|
- {
|
|
|
- this.RaisePropertyChanged(nameof(DownloadedVersionTree));
|
|
|
- IsControlsEnabled = true;
|
|
|
- }
|
|
|
- });
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
public Dictionary<string,Entity.Version.Version?> DownloadedVersionsDictionary { get; set; }
|
|
|
|
|
|
public ObservableCollection<Entity.VersionManifest.Version> FilteredVersions
|
|
@@ -667,6 +272,102 @@ public class VersionsDownloaderViewModel : ViewModelBase
|
|
|
set => this.RaiseAndSetIfChanged(ref _isControlsEnabled, value);
|
|
|
}
|
|
|
|
|
|
+ private async void OnFilteredVersionPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
+ {
|
|
|
+ if (e.PropertyName is nameof(FilteredVersion))
|
|
|
+ {
|
|
|
+ await DownloadButtonTextUpdateAsync(FilteredVersion);
|
|
|
+ await GetFilteredVersionDownloadableFeaturesAsync(FilteredVersion);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async void OnDownloadedVersionPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
+ {
|
|
|
+ if (e.PropertyName is nameof(DownloadedVersion))
|
|
|
+ {
|
|
|
+ await CreateNewTreeListAsync(DownloadedVersion);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Task DownloadButtonTextUpdateAsync(Entity.VersionManifest.Version value)
|
|
|
+ {
|
|
|
+ if (value is null)
|
|
|
+ return Task.CompletedTask;
|
|
|
+
|
|
|
+ if (value.Type == "modpack")
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (System.IO.File.Exists(Settings.minecraftForlderPath + $"versions/{value.Id}/revision.json"))
|
|
|
+ {
|
|
|
+ if (value.ComplianceLevel > JsonSerializer.Deserialize<int>(System.IO.File.ReadAllText(Settings.minecraftForlderPath + $"versions/{value.Id}/revision.json")))
|
|
|
+ DownloadButtonText = "Update Modpack";
|
|
|
+ else
|
|
|
+ DownloadButtonText = "Reinstall Modpack";
|
|
|
+ }
|
|
|
+ else
|
|
|
+ if (System.IO.Directory.Exists($"{Settings.minecraftForlderPath}versions/{value.Id}") && System.IO.File.Exists($"{Settings.minecraftForlderPath}versions/{value.Id}/{value.Id}.json"))
|
|
|
+ DownloadButtonText = "Update Modpack";
|
|
|
+ else
|
|
|
+ DownloadButtonText = "Download Modpack";
|
|
|
+ }
|
|
|
+ catch (Exception)
|
|
|
+ {
|
|
|
+ DownloadButtonText = "Update Modpack";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ if (System.IO.File.Exists(Settings.minecraftForlderPath + $"versions/{value.Id}/{value.Id}.json"))
|
|
|
+ DownloadButtonText = "Reinstall";
|
|
|
+ else
|
|
|
+ DownloadButtonText = "Download";
|
|
|
+ }
|
|
|
+ return Task.CompletedTask;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task GetFilteredVersionDownloadableFeaturesAsync(Entity.VersionManifest.Version value)
|
|
|
+ {
|
|
|
+ _filteredVersionTokenSource.Cancel();
|
|
|
+ _filteredVersionTokenSource.Dispose();
|
|
|
+ _filteredVersionTokenSource = new();
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (await Downloader.IsFileAvaliable(@$"https://files.veloe.link/launcher/forge/Forge{value.Id}/Forge{value.Id}.json", _filteredVersionTokenSource.Token))
|
|
|
+ {
|
|
|
+ if (await Downloader.IsFileAvaliable(@$"https://files.veloe.link/launcher/forge/Forge{value.Id}/Optifine{value.Id}.jar", _filteredVersionTokenSource.Token))
|
|
|
+ InstallForgeOptifineVisible = true;
|
|
|
+ InstallForgeVisible = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ InstallForgeVisible = false;
|
|
|
+ InstallForgeOptifineVisible = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (await Downloader.IsFileAvaliable(@$"https://files.veloe.link/launcher/fabric/Fabric{value.Id}/Fabric{value.Id}.json", _filteredVersionTokenSource.Token))
|
|
|
+ InstallFabricVisible = true;
|
|
|
+ else
|
|
|
+ InstallFabricVisible = false;
|
|
|
+
|
|
|
+ if (await Downloader.IsFileAvaliable(@$"https://files.veloe.link/launcher/optifine/Optifine{value.Id}/Optifine{value.Id}.json", _filteredVersionTokenSource.Token))
|
|
|
+ InstallOptifineVisible = true;
|
|
|
+ else
|
|
|
+ InstallOptifineVisible = false;
|
|
|
+ }
|
|
|
+ catch (OperationCanceledException)
|
|
|
+ {
|
|
|
+ InstallForgeVisible = false;
|
|
|
+ InstallForgeOptifineVisible = false;
|
|
|
+ InstallFabricVisible = false;
|
|
|
+ InstallOptifineVisible = false;
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ OpenErrorWindow(ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
public async Task OnStartBunttonClick()
|
|
|
{
|
|
|
await Task.Run(async () => {
|
|
@@ -723,6 +424,329 @@ public class VersionsDownloaderViewModel : ViewModelBase
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ private async Task CreateNewTreeListAsync(DownloadedVersion value)
|
|
|
+ {
|
|
|
+ await Task.Run(async() =>
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ IsControlsEnabled = false;
|
|
|
+ DownloadedVersionTree.Clear();
|
|
|
+ DownloadedVersionTree = new();
|
|
|
+ this.RaisePropertyChanged(nameof(DownloadedVersionTree));
|
|
|
+ DownloadedVersionsDictionary.Clear();
|
|
|
+
|
|
|
+ if (value is null)
|
|
|
+ {
|
|
|
+ IsControlsEnabled = true;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Entity.Version.Version? version = null;
|
|
|
+
|
|
|
+ foreach (var dversion in DownloadedVersions)
|
|
|
+ {
|
|
|
+ using StreamReader reader = new StreamReader(dversion.path);
|
|
|
+
|
|
|
+ Entity.Version.Version versionObject = await JsonSerializer.DeserializeAsync<Entity.Version.Version>(reader.BaseStream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
|
|
+ DownloadedVersionsDictionary.Add(dversion.version, versionObject);
|
|
|
+
|
|
|
+ if (dversion == _downloadedVersion)
|
|
|
+ {
|
|
|
+ version = versionObject;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (version is null)
|
|
|
+ {
|
|
|
+ OpenErrorWindow("Json file is invalid!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var versionDir = new FileInfo(value.path)?.Directory?.FullName;
|
|
|
+
|
|
|
+ if (versionDir is null)
|
|
|
+ {
|
|
|
+ OpenErrorWindow("Version folder is invalid!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ //get version
|
|
|
+
|
|
|
+ DownloadedVersionTree.Add(new TreeNode() { Title = "Version: " + (version.InheritsFrom ?? version.Id) });
|
|
|
+
|
|
|
+ //calc Size
|
|
|
+
|
|
|
+ var allSize = 0L;
|
|
|
+ var dirSize = 0L;
|
|
|
+ var dirInfo = new DirectoryInfo(versionDir);
|
|
|
+ var sizeTreeNode = new TreeNode();
|
|
|
+ var dirTreeNode = new TreeNode();
|
|
|
+
|
|
|
+ dirTreeNode.SubNode.AddRange(DirectoryToTreeNode(dirInfo, out dirSize));
|
|
|
+ dirTreeNode.Title = "versions/" + dirInfo.Name + " (" + BytesToString(dirSize) + ")";
|
|
|
+ sizeTreeNode.SubNode.Add(dirTreeNode);
|
|
|
+ allSize += dirSize;
|
|
|
+
|
|
|
+ if (version.InheritsFrom is null)
|
|
|
+ {
|
|
|
+ dirInfo = new DirectoryInfo(Settings.minecraftForlderPath);
|
|
|
+ sizeTreeNode.SubNode.AddRange(
|
|
|
+ DirectoryToTreeNode(
|
|
|
+ dirInfo,
|
|
|
+ out dirSize,
|
|
|
+ (dir) => dir.Name == ".mixin.out" ||
|
|
|
+ dir.Name == "assets" ||
|
|
|
+ dir.Name == "javaruntime" ||
|
|
|
+ dir.Name == "libraries" ||
|
|
|
+ dir.Name == "logs" ||
|
|
|
+ dir.Name == "versions",
|
|
|
+ (file) => file.Name.Contains("Veloe") ||
|
|
|
+ file.Name.Contains("log") ||
|
|
|
+ file.Name.Contains("exe") ||
|
|
|
+ file.Name == "launcher_profiles.json" ||
|
|
|
+ file.Name == "libHarfBuzzSharp.dll" ||
|
|
|
+ file.Name == "libSkiaSharp.dll" ||
|
|
|
+ file.Name == "settings.json")
|
|
|
+ );
|
|
|
+ allSize += dirSize;
|
|
|
+ }
|
|
|
+
|
|
|
+ sizeTreeNode.Title = "Size: " + BytesToString(allSize);
|
|
|
+ DownloadedVersionTree.Add(sizeTreeNode);
|
|
|
+
|
|
|
+ //calc Worlds
|
|
|
+
|
|
|
+ dirInfo = null;
|
|
|
+
|
|
|
+ if (version.InheritsFrom is null && Directory.Exists(Settings.minecraftForlderPath + "saves"))
|
|
|
+ dirInfo = new DirectoryInfo(Settings.minecraftForlderPath + "saves");
|
|
|
+ else
|
|
|
+ if (Directory.Exists(versionDir + "/saves"))
|
|
|
+ dirInfo = new DirectoryInfo(versionDir + "/saves");
|
|
|
+
|
|
|
+ if (dirInfo is not null)
|
|
|
+ {
|
|
|
+ var worldsTreeNode = new TreeNode() { Title = "Worlds: " + dirInfo.GetDirectories().Count() };
|
|
|
+
|
|
|
+ foreach (var world in dirInfo.GetDirectories())
|
|
|
+ { worldsTreeNode.SubNode.Add(new TreeNode() { Title = world.Name }); }
|
|
|
+
|
|
|
+ DownloadedVersionTree.Add(worldsTreeNode);
|
|
|
+ }
|
|
|
+
|
|
|
+ //check modloader
|
|
|
+
|
|
|
+ var modsTreeNode = new TreeNode();
|
|
|
+
|
|
|
+ if (version.InheritsFrom is null)
|
|
|
+ modsTreeNode.Title = "Modloader: No";
|
|
|
+ else if (version.Libraries.Any(l => l.Downloads?.Artifact?.Url?.Contains("forge") ?? false))
|
|
|
+ {
|
|
|
+ modsTreeNode.Title = "Modloader: Forge";
|
|
|
+ OpenErrorWindow("This version contains forge modloader, it installs some libraries that can't be displayed and deleted in current launcher versions manager.");
|
|
|
+ }
|
|
|
+ else if (version.Libraries.Any(l => l.Name.Contains("fabric")))
|
|
|
+ modsTreeNode.Title = "Modloader: Fabric";
|
|
|
+
|
|
|
+ //get mods list
|
|
|
+
|
|
|
+ if (modsTreeNode.Title != "No" && Directory.Exists(versionDir + "/mods"))
|
|
|
+ {
|
|
|
+ dirInfo = new DirectoryInfo(versionDir + "/mods");
|
|
|
+
|
|
|
+ modsTreeNode.Title += " (" + dirInfo.EnumerateFiles("*.jar", SearchOption.TopDirectoryOnly).Count() + ")";
|
|
|
+ foreach (var mod in dirInfo.EnumerateFiles("*.jar", SearchOption.TopDirectoryOnly))
|
|
|
+ { modsTreeNode.SubNode.Add(new TreeNode() { Title = mod.Name }); }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ DownloadedVersionTree.Add(modsTreeNode);
|
|
|
+
|
|
|
+ //calc resourcepacks
|
|
|
+
|
|
|
+ dirInfo = null;
|
|
|
+
|
|
|
+ if (version.InheritsFrom is null && Directory.Exists(Settings.minecraftForlderPath + "resourcepacks"))
|
|
|
+ dirInfo = new DirectoryInfo(Settings.minecraftForlderPath + "resourcepacks");
|
|
|
+ else
|
|
|
+ if (Directory.Exists(versionDir + "/resourcepacks"))
|
|
|
+ dirInfo = new DirectoryInfo(versionDir + "/resourcepacks");
|
|
|
+
|
|
|
+ if (dirInfo is not null)
|
|
|
+ {
|
|
|
+ var resourcepacksTreeNode = new TreeNode();
|
|
|
+
|
|
|
+ resourcepacksTreeNode.Title = "Resoursepacks: " + (dirInfo?.GetDirectories()?.Count() + dirInfo?.GetFiles().Count() ?? 0).ToString();
|
|
|
+ foreach (var resourcepack in dirInfo?.GetFileSystemInfos())
|
|
|
+ { resourcepacksTreeNode.SubNode.Add(new TreeNode() { Title = resourcepack.Name }); }
|
|
|
+
|
|
|
+ DownloadedVersionTree.Add(resourcepacksTreeNode);
|
|
|
+ }
|
|
|
+
|
|
|
+ //calc assets
|
|
|
+
|
|
|
+ var assetsFolderName = version.Assets;
|
|
|
+
|
|
|
+ if (version.Assets is null &&
|
|
|
+ version.InheritsFrom is not null &&
|
|
|
+ DownloadedVersionsDictionary.TryGetValue(version.InheritsFrom, out var outVersion) &&
|
|
|
+ outVersion?.Assets is not null)
|
|
|
+ {
|
|
|
+ assetsFolderName = outVersion.Assets;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Directory.Exists(Settings.minecraftForlderPath + "assets/" + assetsFolderName))
|
|
|
+ {
|
|
|
+ dirInfo = new DirectoryInfo(Settings.minecraftForlderPath + "assets/" + assetsFolderName);
|
|
|
+ allSize = 0;
|
|
|
+ foreach (var file in dirInfo.EnumerateFiles("*", SearchOption.AllDirectories))
|
|
|
+ { allSize += file.Length; }
|
|
|
+
|
|
|
+ var assetsTreeNode = new TreeNode();
|
|
|
+ var usedByVersionCount = 0;
|
|
|
+
|
|
|
+ foreach (var dictionaryVersion in DownloadedVersionsDictionary)
|
|
|
+ {
|
|
|
+ if ((dictionaryVersion.Value?.Assets == assetsFolderName &&
|
|
|
+ dictionaryVersion.Value?.InheritsFrom is null) ||
|
|
|
+ (dictionaryVersion.Value?.InheritsFrom is not null &&
|
|
|
+ DownloadedVersionsDictionary.TryGetValue(dictionaryVersion.Value.InheritsFrom, out outVersion) &&
|
|
|
+ outVersion?.Assets == assetsFolderName))
|
|
|
+ {
|
|
|
+ usedByVersionCount++;
|
|
|
+ assetsTreeNode.SubNode.Add(new TreeNode() { Title = dictionaryVersion.Key });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ assetsTreeNode.Title = "Assets: " + version.Assets + " (" + BytesToString(allSize) + ") (" + usedByVersionCount + ")";
|
|
|
+ DownloadedVersionTree.Add(assetsTreeNode);
|
|
|
+ }
|
|
|
+
|
|
|
+ // calc libraries
|
|
|
+
|
|
|
+ var usedLibraries = version.Libraries;
|
|
|
+
|
|
|
+ var libTreeNode = new TreeNode();
|
|
|
+ libTreeNode.Title = "Libraries";
|
|
|
+
|
|
|
+ if (version.InheritsFrom is not null && DownloadedVersionsDictionary.TryGetValue(version.InheritsFrom, out outVersion) && outVersion is not null)
|
|
|
+ {
|
|
|
+ usedLibraries.AddRange(outVersion.Libraries);
|
|
|
+ }
|
|
|
+
|
|
|
+ usedLibraries =
|
|
|
+ usedLibraries
|
|
|
+ .Where(l =>
|
|
|
+ File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + l.Downloads?.Artifact?.Path)) ||
|
|
|
+ File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + l.Downloads?.Classifiers?.NativesLinux?.Path)) ||
|
|
|
+ File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + l.Downloads?.Classifiers?.NativesWindows?.Path)) ||
|
|
|
+ File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + l.Downloads?.Classifiers?.NativesWindows64?.Path)) ||
|
|
|
+ File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + l.Downloads?.Classifiers?.NativesWindows32?.Path)) ||
|
|
|
+ File.Exists(Path.Combine(Settings.minecraftForlderPath + "libraries/" + StartCommandBuilder.GetLibPathFromName(l.Name)))
|
|
|
+ )
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ var libraries = GetLibrariesFromVersions(DownloadedVersionsDictionary)
|
|
|
+ .Where(v => usedLibraries.Any(u => v.Value.Name == u.Name))
|
|
|
+ .GroupBy(a => a.Value.Name)
|
|
|
+ .Select(g =>
|
|
|
+ {
|
|
|
+ return new
|
|
|
+ {
|
|
|
+ Name = g.Key,
|
|
|
+ Count = g.Select(a => a.Key).Distinct().Count(),
|
|
|
+ Versions = g.Select(a => a.Key).Distinct(),
|
|
|
+ LibraryUniqueInstances = g.Select(a => a.Value).Distinct() //IEnumerable cause there can be several lib blocks with different rules in one version json
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ foreach (var lib in libraries)
|
|
|
+ {
|
|
|
+ var subLibTreeNode = new TreeNode()
|
|
|
+ {
|
|
|
+ Title = lib.Name + " (" + lib.Count + ")",
|
|
|
+ };
|
|
|
+
|
|
|
+ //if only one version uses it (selected version), then add path to lib in Tag for deletion
|
|
|
+ if (lib.Count == 1)
|
|
|
+ {
|
|
|
+ subLibTreeNode.Tag = lib.LibraryUniqueInstances.Where(l => l.Downloads?.Artifact?.Path is not null).Select(l => l.Downloads?.Artifact?.Path).FirstOrDefault() ?? string.Empty;
|
|
|
+ //if fabric library
|
|
|
+ if (File.Exists(Settings.minecraftForlderPath + "libraries/" + StartCommandBuilder.GetLibPathFromName(lib.Name)))
|
|
|
+ subLibTreeNode.Tag = StartCommandBuilder.GetLibPathFromName(lib.Name);
|
|
|
+ }
|
|
|
+
|
|
|
+ var subLibUsedVersionsTreeNodeHeader = new TreeNode() { Title = "Using in versions:" };
|
|
|
+
|
|
|
+ foreach (var ver in lib.Versions)
|
|
|
+ subLibUsedVersionsTreeNodeHeader.SubNode.Add(new TreeNode() { Title = ver });
|
|
|
+
|
|
|
+ if (subLibUsedVersionsTreeNodeHeader.SubNode.Count > 0)
|
|
|
+ subLibTreeNode.SubNode.Add(subLibUsedVersionsTreeNodeHeader);
|
|
|
+
|
|
|
+ if (lib.LibraryUniqueInstances.Any(l => l.Natives is not null) || lib.LibraryUniqueInstances.Any(l => l.Downloads?.Classifiers is not null))
|
|
|
+ {
|
|
|
+ var subLibNativesTreeNodeHeader = new TreeNode() { Title = "Natives:" };
|
|
|
+
|
|
|
+ //check classifiers
|
|
|
+ var libraryNativeInstance = lib.LibraryUniqueInstances.Where(l => l.Natives is not null || l.Downloads?.Classifiers is not null).First();
|
|
|
+
|
|
|
+ if (libraryNativeInstance.Downloads?.Classifiers?.NativesWindows is not null)
|
|
|
+ {
|
|
|
+ subLibNativesTreeNodeHeader.SubNode.Add(new TreeNode()
|
|
|
+ {
|
|
|
+ Title = Path.GetFileName(libraryNativeInstance.Downloads?.Classifiers?.NativesWindows.Path) ?? "No lib name",
|
|
|
+ Tag = lib.Count == 1 ? libraryNativeInstance.Downloads?.Classifiers?.NativesWindows?.Path ?? string.Empty : string.Empty
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (libraryNativeInstance.Downloads?.Classifiers?.NativesWindows32 is not null)
|
|
|
+ {
|
|
|
+ subLibNativesTreeNodeHeader.SubNode.Add(new TreeNode()
|
|
|
+ {
|
|
|
+ Title = Path.GetFileName(libraryNativeInstance.Downloads?.Classifiers?.NativesWindows32.Path) ?? "No lib name",
|
|
|
+ Tag = lib.Count == 1 ? libraryNativeInstance.Downloads?.Classifiers?.NativesWindows32?.Path ?? string.Empty : string.Empty
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (libraryNativeInstance.Downloads?.Classifiers?.NativesWindows64 is not null)
|
|
|
+ {
|
|
|
+ subLibNativesTreeNodeHeader.SubNode.Add(new TreeNode()
|
|
|
+ {
|
|
|
+ Title = Path.GetFileName(libraryNativeInstance.Downloads?.Classifiers?.NativesWindows64.Path) ?? "No lib name",
|
|
|
+ Tag = lib.Count == 1 ? libraryNativeInstance.Downloads?.Classifiers?.NativesWindows64?.Path ?? string.Empty : string.Empty
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (libraryNativeInstance.Downloads?.Classifiers?.NativesLinux is not null)
|
|
|
+ {
|
|
|
+ subLibNativesTreeNodeHeader.SubNode.Add(new TreeNode()
|
|
|
+ {
|
|
|
+ Title = Path.GetFileName(libraryNativeInstance.Downloads?.Classifiers?.NativesLinux.Path) ?? "No lib name",
|
|
|
+ Tag = lib.Count == 1 ? libraryNativeInstance.Downloads?.Classifiers?.NativesLinux?.Path ?? string.Empty : string.Empty
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (subLibNativesTreeNodeHeader.SubNode.Count > 0)
|
|
|
+ subLibTreeNode.SubNode.Add(subLibNativesTreeNodeHeader);
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ libTreeNode.SubNode.Add(subLibTreeNode);
|
|
|
+ }
|
|
|
+
|
|
|
+ _libraries = libTreeNode;
|
|
|
+ DownloadedVersionTree.Add(libTreeNode);
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ this.RaisePropertyChanged(nameof(DownloadedVersionTree));
|
|
|
+ IsControlsEnabled = true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
public async Task OnDeleteButtonClick()
|
|
|
{
|
|
|
await Task.Run(() => {
|
|
@@ -1007,6 +1031,12 @@ public class VersionsDownloaderViewModel : ViewModelBase
|
|
|
yield return new KeyValuePair<string, Library>(version.Key,lib);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ public void Dispose()
|
|
|
+ {
|
|
|
+ this.PropertyChanged -= OnFilteredVersionPropertyChanged;
|
|
|
+ this.PropertyChanged -= OnDownloadedVersionPropertyChanged;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
public class TreeNode
|