Browse Source

replaced obsolete WebClient on HttpClient in downloader class, added async assets download, changed versions downloader window ui

Veloe 2 years ago
parent
commit
64a8243c56

+ 299 - 236
VeloeMinecraftLauncher/MinecraftLauncher/Downloader.cs

@@ -1,44 +1,135 @@
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Threading;
-using System;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.IO;
+using System.IO.Compression;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Handlers;
 using System.Text.Json;
+using System.Threading;
 using System.Threading.Tasks;
-using System.IO.Compression;
 using VeloeMinecraftLauncher.Entity.Assets;
-using VeloeMinecraftLauncher.ViewModels;
-using VeloeMinecraftLauncher.Views;
-using System.Collections.ObjectModel;
 using VeloeMinecraftLauncher.Models.Entity;
-using SkiaSharp;
 
 namespace VeloeMinecraftLauncher.MinecraftLauncher;
 
 internal static class Downloader
 {
+    static HttpClient _httpClient = new HttpClient();
+
     public static async Task<TaskStatus> StartDownload(
+        Action<string> DownloadingFileName, //status text showing on ui
+        Action<string> TaskStatus, //
+        Action<bool> IsControlsEnabled, //value locking ui buttons
+        Action<long> SetProgress, // progress bar
+        Entity.Version.Version versionJson,
+        CancellationToken cancellationToken,
+        bool DownloadJava = false,
+        bool InstallForge = false,
+        bool InstallOptifine = false,
+        bool InstallForgeOptifine = false,
+        bool InstallFabric = false)
+    {
+        return await DownloadClient(DownloadingFileName, TaskStatus, IsControlsEnabled, SetProgress, versionJson, cancellationToken, 
+                                    DownloadJava, InstallForge, InstallOptifine, InstallForgeOptifine, InstallFabric);
+    }
+
+    public static async Task<TaskStatus> StartDownloadModpack(
         Action<string> DownloadingFileName,
+        Action<string> TaskStatus,
         Action<bool> IsControlsEnabled,
-        Action<WebClient> SetProgressBar,
-        Action<WebClient> RemoveProgressBar,
-        Entity.Version.Version versionJson,
+        Action<long> SetProgress,
+        ObservableCollection<Entity.VersionManifest.Version> FilteredVersions,
+        Modpack SelectedModpack,
+        CancellationToken cancellationToken,
+        bool DownloadJava = false,
+        bool InstallForge = false,
+        bool InstallOptifine = false,
+        bool InstallForgeOptifine = false,
+        bool InstallFabric = false)
+    {
+        return await DownloadClient(DownloadingFileName, TaskStatus, IsControlsEnabled, SetProgress, null, cancellationToken, DownloadJava,
+                                    InstallForge, InstallOptifine, InstallForgeOptifine, InstallFabric,FilteredVersions,SelectedModpack,true);
+    }
+
+    private static async Task<TaskStatus> DownloadClient(
+        Action<string> DownloadingFileName, //status text showing on ui
+        Action<string> TasksStatus,
+        Action<bool> IsControlsEnabled, //value locking ui buttons
+        Action<long> SetProgress, // progress bar
+        Entity.Version.Version? versionJson,
+        CancellationToken cancellationToken,
         bool DownloadJava = false,
         bool InstallForge = false,
         bool InstallOptifine = false,
         bool InstallForgeOptifine = false,
-        bool InstallFabric = false
-        )
+        bool InstallFabric = false,
+        ObservableCollection<Entity.VersionManifest.Version>? FilteredVersions = null,
+        Modpack? SelectedModpack = null,
+        bool downloadModpack = false)
     {
         try
         {
+            //task 1 - download java
+            //task 2 - download jar
+            //task 3 - download libs
+            //task 4 - download assets
+            //task 5 - download addons
+            //task 6 - download modpacks
+            IsControlsEnabled(false);
+            SetProgress( 0 );
+            var tasksCount = 3;
+            var tasksIterator = 0;
+
+            if (DownloadJava) tasksCount++;
+            if (InstallForge || InstallOptifine || InstallForgeOptifine || InstallFabric) tasksCount++;
+            if (downloadModpack) tasksCount++;
+
+            TasksStatus($"Task {tasksIterator} of {tasksCount} complete.");
+
+            var handler = new HttpClientHandler() { AllowAutoRedirect = true };
+            var ph = new ProgressMessageHandler(handler);
+            ph.HttpSendProgress += (_, args) =>
+            {
+                SetProgress(args.ProgressPercentage);
+            };
+
+            ph.HttpReceiveProgress += (_, args) =>
+            {
+                SetProgress(args.ProgressPercentage);
+            };
+
+            using var httpClient = new HttpClient(ph);
+
+            Entity.VersionManifest.Version? filteredVersion;
+
+            if (downloadModpack)
+            {
+                if (FilteredVersions is null || SelectedModpack is null)
+                    return TaskStatus.Faulted;
+
+                filteredVersion = FilteredVersions.Where(x => x.Id == SelectedModpack.Version).FirstOrDefault();
+
+                if (filteredVersion is null)
+                    return TaskStatus.Faulted;
+
+                InstallForge = SelectedModpack.Forge;
+                InstallForgeOptifine = SelectedModpack.ForgeOptifine;
+                InstallOptifine = SelectedModpack.Optifine;
+                InstallFabric = SelectedModpack.Fabric;
+
+                Settings.logger.Debug("Downloading {0}.json", filteredVersion.Id);
+                DownloadingFileName($"{filteredVersion.Id}.json");
+                versionJson = await Downloader.DownloadAndDeserializeJsonData<Entity.Version.Version>(filteredVersion.Url, Settings.minecraftForlderPath + "versions/" + filteredVersion.Id + "/", filteredVersion.Id + ".json");
+            }
+
+            if (versionJson is null)
+                return TaskStatus.Faulted;
+            //SetMaxProgressBarValue(size);
             Settings.logger.Debug("Downloading minecraft {0}", versionJson.Id);
-            var webClient = new WebClient();
-            
-            SetProgressBar(webClient);
-            //webClient.DownloadProgressChanged += WebClient_DownloadProgressChanged;
+
             //download java
             if (DownloadJava)
             {
@@ -79,7 +170,7 @@ internal static class Downloader
                                 javaUrl = "https://files.veloe.link/launcher/java/18/linux64/java.zip";
 
                             break;
-                    }                   
+                    }
                 }
 
                 if (versionJson.Assets == "legacy")
@@ -99,9 +190,8 @@ internal static class Downloader
                 {
                     Settings.logger.Debug("Downloading Java");
                     DownloadingFileName("java.zip");
-                    webClient.DownloadFileAsync(new System.Uri(javaUrl), Settings.minecraftForlderPath + "java.zip");
-                    waitWhileBisy(ref webClient);
-
+                    await DownloadFileAsync(javaUrl, Settings.minecraftForlderPath + "java.zip", cancellationToken, httpClient);
+                     
                     Settings.logger.Debug("Unpacking Java");
                     DownloadingFileName("Unpacking java.zip");
                     ZipFile.ExtractToDirectory($"{Settings.minecraftForlderPath}java.zip", Settings.minecraftForlderPath, true);
@@ -109,20 +199,17 @@ internal static class Downloader
                 }
                 else
                     Settings.logger.Debug("Required Java version don't found in json file."); //log that java was not downloaded;
-            }
 
+                tasksIterator++;
+                TasksStatus($"Task {tasksIterator} of {tasksCount} complete.");
+            }
             //download json
             var path = Settings.minecraftForlderPath + "/versions/" + versionJson.Id;
             if (!Directory.Exists(path))
             {
                 Settings.logger.Debug("Creating path: {0}", path);
                 Directory.CreateDirectory(path);
-            }
-
-            //Settings.logger.Debug("Downloading {0}", $"{versionJson.id}.json");
-            //DownloadingFileName($"{versionJson.id}.json");
-            //webClient.DownloadFileAsync(new System.Uri(versionJson.url), path + "/" + versionJson.id + ".json");
-            //waitWhileBisy(ref webClient);
+            }           
 
             //download jar
             if (!Directory.Exists(path))
@@ -148,12 +235,15 @@ internal static class Downloader
             {
                 Settings.logger.Debug("Downloading {0}", $"{versionJson.Id}.jar");
                 DownloadingFileName($"{versionJson.Id}.jar");
-                webClient.DownloadFileAsync(new System.Uri(versionJson.Downloads.Client.Url), path + "/" + versionJson.Id + ".jar");
-                waitWhileBisy(ref webClient);
+                await DownloadFileAsync(versionJson.Downloads.Client.Url, path + "/" + versionJson.Id + ".jar", cancellationToken, httpClient);
+                 
             }
+            tasksIterator++;
+            TasksStatus($"Task {tasksIterator} of {tasksCount} complete.");
             //end download jar
             //download libraries
             Settings.logger.Debug("Downloading libraries.");
+
             foreach (var library in versionJson.Libraries)
             {
                 var libPath = Settings.minecraftForlderPath + "libraries/";
@@ -214,7 +304,7 @@ internal static class Downloader
 
                 //checking rules
                 if (library.Rules == null)
-                {                 
+                {
                     if (!Directory.Exists(libDir))
                     {
                         Settings.logger.Debug("Creating path: {0}", path);
@@ -238,8 +328,7 @@ internal static class Downloader
                     {
                         Settings.logger.Debug("Downloading: {0}", libName);
                         DownloadingFileName(libName);
-                        webClient.DownloadFileAsync(new System.Uri(libUrl), libDir + "/" + libName);
-                        waitWhileBisy(ref webClient);
+                        await DownloadFileAsync(libUrl, libDir + "/" + libName, cancellationToken, httpClient);                        
                     }
                 }
                 else
@@ -271,8 +360,7 @@ internal static class Downloader
                             {
                                 Settings.logger.Debug("Downloading: {0}", libName);
                                 DownloadingFileName(libName);
-                                webClient.DownloadFileAsync(new System.Uri(libUrl), libDir + "/" + libName);
-                                waitWhileBisy(ref webClient);
+                                await DownloadFileAsync(libUrl, libDir + "/" + libName, cancellationToken, httpClient);
                             }
                             continue;
                         }
@@ -288,7 +376,7 @@ internal static class Downloader
                             Settings.logger.Debug("Downloading: {0}", libName);
                             DownloadingFileName = libName;
                             webClient.DownloadFileAsync(new System.Uri(libUrl), libPath + "/" + libName);
-                            waitWhileBisy(ref webClient);
+                             
                         }
                         */
                     }
@@ -313,15 +401,18 @@ internal static class Downloader
                         Settings.logger.Debug("Extracting {0} to {1}", libName, Settings.minecraftForlderPath + "versions/" + versionJson.Id + "/natives/");
                         ZipFile.ExtractToDirectory(libDir + "/" + libName, Settings.minecraftForlderPath + "versions/" + versionJson.Id + "/natives/", true);
                     }
-                    catch (IOException ex)
+                    catch (IOException)
                     {
-                        Settings.logger.Error("IOException in VersionsDownloaderViewModel on native lib extraction");
+                        Settings.logger.Error("IOException on native lib extraction");
                     }
                     //TODO delete META-INF and sha1 files after
                 }
             }
 
-            var assetsJson = Downloader.DownloadAndDeserializeJsonData<AssetsManifest>(versionJson.AssetIndex.Url);
+            tasksIterator++;
+            TasksStatus($"Task {tasksIterator} of {tasksCount} complete.");
+
+            var assetsJson = await Downloader.DownloadAndDeserializeJsonData<AssetsManifest>(versionJson.AssetIndex.Url);
             var assetsPath = Settings.minecraftForlderPath + "assets/" + versionJson.Assets + "/objects";
             var assetsUrl = "https://resources.download.minecraft.net/";
 
@@ -330,37 +421,56 @@ internal static class Downloader
 
             if (!Directory.Exists(Settings.minecraftForlderPath + "/assets/" + versionJson.Assets + "/indexes/"))
                 Directory.CreateDirectory(Settings.minecraftForlderPath + "/assets/" + versionJson.Assets + "/indexes/");
-            webClient.DownloadFileAsync(new System.Uri(versionJson.AssetIndex.Url), Settings.minecraftForlderPath + "/assets/" + versionJson.Assets + "/indexes/" + versionJson.Assets + ".json");
-            waitWhileBisy(ref webClient);
+            await DownloadFileAsync(versionJson.AssetIndex.Url, Settings.minecraftForlderPath + "/assets/" + versionJson.Assets + "/indexes/" + versionJson.Assets + ".json", cancellationToken);
 
-            //download assets
-            foreach (var asset in assetsJson.Objects)
-            {
-                var folder = asset.Value.Hash.Substring(0, 2);
-                if (!Directory.Exists(assetsPath + "/" + folder))
-                    Directory.CreateDirectory(assetsPath + "/" + folder);
+            Settings.logger.Debug("Downloading assets.");
+            SetProgress(0);
+            var assetsCount = assetsJson.Objects.Count;
+            var assetsIterator = 1;
 
-                chksum = String.Empty;
-                //here hash check
-                if (File.Exists(assetsPath + "/" + folder + "/" + asset.Value.Hash))
-                {
-                    DownloadingFileName($"Checking hash: {asset.Value.Hash}");
-                    FileStream fop = File.OpenRead(assetsPath + "/" + folder + "/" + asset.Value.Hash);
-                    byte[] hash = System.Security.Cryptography.SHA1.Create().ComputeHash(fop);
-                    chksum = BitConverter.ToString(hash).Replace("-", String.Empty).ToLower();
+            List<Task> assetsDownloadTasks = new();
+            using SemaphoreSlim concurrencySemaphore = new SemaphoreSlim(0,40);
 
-                    fop.Close();
-                    fop.Dispose();
-                }
+            foreach (var asset in assetsJson.Objects.DistinctBy(a => a.Value.Hash))
+            {
+                assetsDownloadTasks.Add(Task.Run(async () => {
+                    
+                    var folder = asset.Value.Hash.Substring(0, 2);
 
-                if (chksum != asset.Value.Hash)
-                {
-                    Settings.logger.Debug("Downloading: {0}", asset.Value.Hash);
-                    DownloadingFileName($"Asset {asset.Value.Hash}");
-                    webClient.DownloadFileAsync(new System.Uri(assetsUrl + folder + "/" + asset.Value.Hash), assetsPath + "/" + folder + "/" + asset.Value.Hash);
-                    waitWhileBisy(ref webClient);
-                }
+                    var chksum = String.Empty;
+                    //here hash check
+                    if (File.Exists(assetsPath + "/" + folder + "/" + asset.Value.Hash))
+                    {
+                        DownloadingFileName($"Checking hash {assetsIterator} of {assetsCount}");
+                        using FileStream fop = File.OpenRead(assetsPath + "/" + folder + "/" + asset.Value.Hash);
+                        byte[] hash = System.Security.Cryptography.SHA1.Create().ComputeHash(fop);
+                        chksum = BitConverter.ToString(hash).Replace("-", String.Empty).ToLower();
+                        fop.Close();
+                    }
+                
+                    if (chksum != asset.Value.Hash)
+                    {
+                        concurrencySemaphore.Wait(cancellationToken);
+                        if (!Directory.Exists(assetsPath + "/" + folder))
+                            Directory.CreateDirectory(assetsPath + "/" + folder);
+
+                        Settings.logger.Debug("Downloading asset: {0} ", asset.Value.Hash);
+                        //DownloadingFileName($"Asset {assetsIterator} of {assetsCount}");
+                        DownloadingFileName($"Asset {assetsIterator} of {assetsCount}");                                                      
+                        await DownloadFileAsync(assetsUrl + folder + "/" + asset.Value.Hash, assetsPath + "/" + folder + "/" + asset.Value.Hash, cancellationToken);                                                  
+                        concurrencySemaphore.Release();
+                    }
+                    SetProgress(assetsIterator * 100 / assetsCount);
+                    Interlocked.Increment(ref assetsIterator);
+                    
+                }));
             }
+            concurrencySemaphore.Release(40);
+            Task.WaitAll(assetsDownloadTasks.ToArray());
+
+            SetProgress(100);
+            tasksIterator++;
+            TasksStatus($"Task {tasksIterator} of {tasksCount} complete.");
 
             if (InstallForge)
             {
@@ -372,17 +482,17 @@ internal static class Downloader
                     Settings.logger.Debug("Creating path: {0}", $"{Settings.minecraftForlderPath}versions/" + forgePath);
                     Directory.CreateDirectory($"{Settings.minecraftForlderPath}versions/" + forgePath);
                 }
-                waitWhileBisy(ref webClient);
+                 
 
                 Settings.logger.Debug("Downloading: {0}", $"Forge{versionJson.Id}.json");
                 DownloadingFileName($"Forge{versionJson.Id}.json");
-                webClient.DownloadFileAsync(new System.Uri($"{forgeUrl}Forge{versionJson.Id}.json"), Settings.minecraftForlderPath + "versions/" + forgePath + "/" + forgePath + ".json");
-                waitWhileBisy(ref webClient);
+                await DownloadFileAsync($"{forgeUrl}Forge{versionJson.Id}.json", Settings.minecraftForlderPath + "versions/" + forgePath + "/" + forgePath + ".json", cancellationToken, httpClient);
+                 
 
                 Settings.logger.Debug("Downloading: {0}", "libraries.zip");
                 DownloadingFileName("libraries.zip");
-                webClient.DownloadFileAsync(new System.Uri(forgeUrl + "libraries.zip"), Settings.minecraftForlderPath + "versions/" + forgePath + "/libraries.zip");
-                waitWhileBisy(ref webClient);
+                await DownloadFileAsync(forgeUrl + "libraries.zip", Settings.minecraftForlderPath + "versions/" + forgePath + "/libraries.zip", cancellationToken, httpClient);
+                 
 
                 Settings.logger.Debug("Extracting: {0}", "libraries.zip");
                 DownloadingFileName("Unpacking libraries.zip");
@@ -395,26 +505,26 @@ internal static class Downloader
                 var optifinePath = $"Optifine{versionJson.Id}";
                 var optifineUrl = $"https://files.veloe.link/launcher/optifine/Optifine{versionJson.Id}/";
 
-                if (!Directory.Exists($"{Settings.minecraftForlderPath}versions/Optifine{versionJson.Id}"))
+                if (!Directory.Exists($"{Settings.minecraftForlderPath}versions/{optifinePath}"))
                 {
-                    Settings.logger.Debug("Creating path: {0}", $"{Settings.minecraftForlderPath}Optifine/" + optifinePath);
-                    Directory.CreateDirectory($"{Settings.minecraftForlderPath}Optifine/" + optifinePath);
+                    Settings.logger.Debug("Creating path: {0}", $"{Settings.minecraftForlderPath}versions/" + optifinePath);
+                    Directory.CreateDirectory($"{Settings.minecraftForlderPath}versions/" + optifinePath);
                 }
 
                 Settings.logger.Debug("Downloading: {0}", $"Optifine{versionJson.Id}.json");
                 DownloadingFileName($"Optifine{versionJson.Id}.json");
-                webClient.DownloadFileAsync(new System.Uri($"{optifineUrl}Optifine{versionJson.Id}.json"), Settings.minecraftForlderPath + "versions/" + optifinePath + "/" + optifinePath + ".json");
-                waitWhileBisy(ref webClient);
+                await DownloadFileAsync($"{optifineUrl}Optifine{versionJson.Id}.json", Settings.minecraftForlderPath + "versions/" + optifinePath + "/" + optifinePath + ".json", cancellationToken, httpClient);
+                 
 
                 Settings.logger.Debug("Downloading: {0}", $"Optifine{versionJson.Id}.jar");
                 DownloadingFileName($"Optifine{versionJson.Id}.json");
-                webClient.DownloadFileAsync(new System.Uri($"{optifineUrl}Optifine{versionJson.Id}.jar"), Settings.minecraftForlderPath + "versions/" + optifinePath + "/" + optifinePath + ".jar");
-                waitWhileBisy(ref webClient);
+                await DownloadFileAsync($"{optifineUrl}Optifine{versionJson.Id}.jar", Settings.minecraftForlderPath + "versions/" + optifinePath + "/" + optifinePath + ".jar", cancellationToken, httpClient);
+                 
 
                 Settings.logger.Debug("Downloading: {0}", "libraries.zip");
                 DownloadingFileName("libraries.zip");
-                webClient.DownloadFileAsync(new System.Uri(optifineUrl + "libraries.zip"), Settings.minecraftForlderPath + "versions/" + optifinePath + "/libraries.zip");
-                waitWhileBisy(ref webClient);
+                await DownloadFileAsync(optifineUrl + "libraries.zip", Settings.minecraftForlderPath + "versions/" + optifinePath + "/libraries.zip", cancellationToken, httpClient);
+                 
 
                 Settings.logger.Debug("Extracting: {0}", "libraries.zip");
                 DownloadingFileName("Unpacking libraries.zip");
@@ -432,199 +542,152 @@ internal static class Downloader
 
                 Settings.logger.Debug("Downloading: {0}", $"Optifine{versionJson.Id}.jar");
                 DownloadingFileName($"Optifine{versionJson.Id}.jar");
-                webClient.DownloadFileAsync(new System.Uri(@$"https://files.veloe.link/launcher/forge/Forge{versionJson.Id}/Optifine{versionJson.Id}.jar"),
-                Settings.minecraftForlderPath + "versions/Forge" + versionJson.Id + "/mods/" + "Optifine" + versionJson.Id + ".jar");
-                waitWhileBisy(ref webClient);
+                await DownloadFileAsync($@"https://files.veloe.link/launcher/forge/Forge{versionJson.Id}/Optifine{versionJson.Id}.jar", Settings.minecraftForlderPath + "versions/Forge" + versionJson.Id + "/mods/" + "Optifine" + versionJson.Id + ".jar", cancellationToken, httpClient); 
             }
 
-            RemoveProgressBar(webClient);
-            //Progress = 100;
-            Settings.logger.Debug("Downloading finished.");
-            DownloadingFileName("Finished!");
-            IsControlsEnabled(true);
-            
-            webClient.Dispose();
-        }
-        catch (Exception ex)
-        {
-            IsControlsEnabled(true);
-            DownloadingFileName($"Error occured while downloading {versionJson.Id}.");
-            Settings.logger.Error(ex.Message);
-            if (ex.StackTrace is not null)
-                Settings.logger.Error(ex.StackTrace);
-            return TaskStatus.Faulted;
-        }
-        return TaskStatus.RanToCompletion;
-    }
-
-    public static async Task<TaskStatus> StartDownloadModpack(Action<string> DownloadingFileName,
-        Action<bool> IsControlsEnabled,
-        Action<WebClient> SetProgressBar,
-        Action<WebClient> RemoveProgressBar,
-        ObservableCollection<Entity.VersionManifest.Version> FilteredVersions,
-        Modpack SelectedModpack,
-        bool DownloadJava = false,
-        bool InstallForge = false,
-        bool InstallOptifine = false,
-        bool InstallForgeOptifine = false,
-        bool InstallFabric = false)
-    {
-        try
-        {
-            var FilteredVersion = FilteredVersions.Where(x => x.Id == SelectedModpack.Version).FirstOrDefault();
-
-            if (FilteredVersion is null)
-                return TaskStatus.Faulted;
-
-            IsControlsEnabled(false);
-
-            InstallForge = SelectedModpack.Forge;
-            InstallForgeOptifine = SelectedModpack.ForgeOptifine;
-            InstallOptifine = SelectedModpack.Optifine;
-            InstallFabric = SelectedModpack.Fabric;
-
-            Settings.logger.Debug("Downloading {0}.json", FilteredVersion.Id);
-            DownloadingFileName($"{FilteredVersion.Id}.json");
-            var versionJson = Downloader.DownloadAndDeserializeJsonData<Entity.Version.Version>(FilteredVersion.Url, Settings.minecraftForlderPath + "versions/" + FilteredVersion.Id + "/", FilteredVersion.Id + ".json");
-
-            await Downloader.StartDownload(
-                DownloadingFileName,
-                IsControlsEnabled,
-                SetProgressBar,
-                RemoveProgressBar,
-                versionJson,
-                DownloadJava,
-                InstallForge,
-                InstallOptifine,
-                InstallForgeOptifine,
-                InstallFabric);
-
-            IsControlsEnabled(false);
-
-            string modpackUrl = string.Empty;
-
-            if (SelectedModpack.Url is not null)
-                modpackUrl = SelectedModpack.Url;
-            else
-                modpackUrl = $"https://files.veloe.link/launcher/modpacks/{SelectedModpack.Name}.zip";
-
-            if (!Directory.Exists($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}"))
+            if (InstallForge || InstallOptifine || InstallForgeOptifine || InstallFabric)
             {
-                Settings.logger.Debug("Creating path: {0}", $"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}");
-                Directory.CreateDirectory($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}");
+                tasksIterator++;
+                TasksStatus($"Task {tasksIterator} of {tasksCount} complete.");
             }
 
-            WebClient webClient = new WebClient();
+            if (downloadModpack)
+            {
+                if (SelectedModpack is null)
+                    return TaskStatus.Faulted;
 
-            SetProgressBar(webClient);
+                string modpackUrl = string.Empty;
 
-            Settings.logger.Debug("Downloading: {0}", $"{SelectedModpack.Name}.zip");
-            DownloadingFileName($"{SelectedModpack.Name}.zip");
-            webClient.DownloadFileAsync(new System.Uri(modpackUrl), Settings.minecraftForlderPath + $"versions/{SelectedModpack.Name}.zip");
-            waitWhileBisy(ref webClient);
+                if (SelectedModpack.Url is not null)
+                    modpackUrl = SelectedModpack.Url;
+                else
+                    modpackUrl = $"https://files.veloe.link/launcher/modpacks/{SelectedModpack.Name}.zip";
 
-            FileStream stream = null;
-            if (Directory.Exists($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/mods"))
-            {
-                foreach (var file in Directory.GetFiles($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/mods"))
+                if (!Directory.Exists($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}"))
                 {
-                    try { stream = File.Open(file,FileMode.Open, FileAccess.Read, FileShare.None);}
-                    catch (IOException) { throw; }
-                    finally { stream?.Close(); }
+                    Settings.logger.Debug("Creating path: {0}", $"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}");
+                    Directory.CreateDirectory($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}");
                 }
-            }
-            if (Directory.Exists($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/config"))
-                foreach (var file in Directory.GetFiles($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/mods"))
+
+                Settings.logger.Debug("Downloading: {0}", $"{SelectedModpack.Name}.zip");
+                DownloadingFileName($"{SelectedModpack.Name}.zip");
+                await DownloadFileAsync(modpackUrl, Settings.minecraftForlderPath + $"versions/{SelectedModpack.Name}.zip", cancellationToken, httpClient);
+
+                FileStream? stream = null;
+
+                if (Directory.Exists($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/mods"))
                 {
-                    try { stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.None); }
-                    catch (IOException) { throw; }
-                    finally { stream?.Close(); }
+                    foreach (var file in Directory.GetFiles($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/mods"))
+                    {
+                        try { stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.None); }
+                        finally { stream?.Close(); }
+                    }
                 }
+                if (Directory.Exists($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/config"))
+                    foreach (var file in Directory.GetFiles($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/mods"))
+                    {
+                        try { stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.None); }
+                        finally { stream?.Close(); }
+                    }
 
-            if (Directory.Exists($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/mods"))
-                Directory.Delete($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/mods", true);
-            if (Directory.Exists($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/config"))
-                Directory.Delete($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/config", true);
+                stream?.Dispose();
 
-            Settings.logger.Debug("Extracting: {0}", $"{SelectedModpack.Name}.zip");
-            DownloadingFileName($"Unpacking {SelectedModpack.Name}.zip");
-            ZipFile.ExtractToDirectory($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}.zip", Settings.minecraftForlderPath + $"versions/{SelectedModpack.Name}", true);
-            File.Delete($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}.zip");
+                if (Directory.Exists($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/mods"))
+                    Directory.Delete($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/mods", true);
+                if (Directory.Exists($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/config"))
+                    Directory.Delete($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/config", true);
 
-            File.WriteAllText($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/revision.json",JsonSerializer.Serialize(SelectedModpack.Revision));
+                Settings.logger.Debug("Extracting: {0}", $"{SelectedModpack.Name}.zip");
+                DownloadingFileName($"Unpacking {SelectedModpack.Name}.zip");
+                ZipFile.ExtractToDirectory($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}.zip", Settings.minecraftForlderPath + $"versions/{SelectedModpack.Name}", true);
+                File.Delete($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}.zip");
 
-            RemoveProgressBar(webClient);
+                File.WriteAllText($"{Settings.minecraftForlderPath}versions/{SelectedModpack.Name}/revision.json", JsonSerializer.Serialize(SelectedModpack.Revision));
+                tasksIterator++;
+                TasksStatus($"Task {tasksIterator} of {tasksCount} complete.");
+            }
 
+            //RemoveProgressBar(webClient);
             //Progress = 100;
-            Settings.logger.Debug("Downloading {0} finished.", SelectedModpack.Name);
-            DownloadingFileName($"Finished {SelectedModpack.Name}!");
+            Settings.logger.Debug("Downloading finished.");
+            DownloadingFileName("Finished!");
             IsControlsEnabled(true);
-            webClient.Dispose();
 
+            //webClient.Dispose();
+        }
+        catch (TaskCanceledException ex)
+        {
+            IsControlsEnabled(true);
+            if (downloadModpack && SelectedModpack is not null)
+                DownloadingFileName($"Downloading {SelectedModpack.Name} canceled.");
+            else
+                DownloadingFileName($"Downloading {versionJson.Id} canceled.");
+            Settings.logger.Warning(ex.Message);
+            return TaskStatus.Canceled;
         }
         catch (Exception ex)
         {
             IsControlsEnabled(true);
-            DownloadingFileName($"Error occured while downloading {SelectedModpack.Name}.");        
-            ViewModelBase.OpenErrorWindow(ex);
+            if (downloadModpack && SelectedModpack is not null)
+                DownloadingFileName($"Error occured while downloading {SelectedModpack.Name}.");
+            else
+                DownloadingFileName($"Error occured while downloading {versionJson.Id}.");
+            Settings.logger.Error(ex.Message);
+            if (ex.StackTrace is not null)
+                Settings.logger.Error(ex.StackTrace);
             return TaskStatus.Faulted;
         }
         return TaskStatus.RanToCompletion;
     }
 
-    public static T DownloadAndDeserializeJsonData<T>(string url, string path = "", string filename = "") where T : new()
+    public static async Task<T> DownloadAndDeserializeJsonData<T>(string url, string path = "", string filename = "") where T : new()
     {
-        using (var webClient = new HttpClient())
+        var jsonData = string.Empty;
+
+        try
         {
-            var jsonData = string.Empty;
-            try
-            {
-                try
-                {
-                    Settings.logger.Debug($"Downloading: {url.Split('/').Last()}");
-                    jsonData = webClient.GetStringAsync(url).Result;
-                }
-                catch (Exception ex)
-                {
-                    throw new WebException($"An error occured while downloading {url}. Check your internet connection.",ex);
-                }
+            Settings.logger.Debug($"Downloading: {url.Split('/').Last()}");
+            jsonData = await _httpClient.GetStringAsync(url);
+        }
+        catch (Exception ex)
+        {
+            throw new WebException($"An error occured while downloading {url}. Check your internet connection.",ex);
+        }
             
-                if (string.IsNullOrEmpty(jsonData))
-                {
-                    Settings.logger.Warning("Empty string!");
-                    return new T();
-                }
-                else
+        if (string.IsNullOrEmpty(jsonData))
+        {
+            Settings.logger.Warning("Empty string!");
+            return new T();
+        }
+        else
+        {
+            if (!string.IsNullOrEmpty(filename) && !string.IsNullOrEmpty(path))
+            {
+                if (!Directory.Exists(path))
                 {
-                    if (!string.IsNullOrEmpty(filename) && !string.IsNullOrEmpty(path))
-                    {
-                        if (!Directory.Exists(path))
-                        {
-                            Settings.logger.Debug("Creating path: {0}", path);
-                            Directory.CreateDirectory(path);
-                        }
-                        File.WriteAllText(path + filename, jsonData);
-                    }
-                    //Settings.logger.Debug("Return serialized string.");
-                    return JsonSerializer.Deserialize<T>(jsonData, new JsonSerializerOptions{PropertyNameCaseInsensitive = true});
+                    Settings.logger.Debug("Creating path: {0}", path);
+                    Directory.CreateDirectory(path);
                 }
-                /*
-                return string.IsNullOrEmpty(jsonData)
-                            ? new T()
-                            : JsonSerializer.Deserialize<T>(jsonData);*/
-            }
-            catch (Exception ex)
-            {
-                throw;
+                File.WriteAllText(path + filename, jsonData);
             }
-        }
+            //Settings.logger.Debug("Return serialized string.");
+            return JsonSerializer.Deserialize<T>(jsonData, new JsonSerializerOptions{PropertyNameCaseInsensitive = true});
+        }       
     }
 
-    private static void waitWhileBisy(ref WebClient webClient)
-    { 
-        while (webClient.IsBusy)
-        {
-            Task.Delay(10).Wait();
-        }
+    private static async Task DownloadFileAsync(string url, string path, CancellationToken ct, HttpClient? client = null)
+    {
+        if (client == null) client = _httpClient;
+        using HttpResponseMessage response = await client.GetAsync(new System.Uri(url),HttpCompletionOption.ResponseHeadersRead, ct);
+        response.EnsureSuccessStatusCode();
+        using Stream contentStream = await response.Content.ReadAsStreamAsync(ct);
+        using FileStream fileStream = File.OpenWrite(path);
+        await contentStream.CopyToAsync(fileStream,ct);
+    }
+
+    public static async Task<bool> IsFileAvaliable(string url, CancellationToken ct)
+    {
+        using HttpResponseMessage response = await _httpClient.GetAsync(new System.Uri(url), HttpCompletionOption.ResponseHeadersRead, ct);
+        return response.IsSuccessStatusCode;
     }
 }

+ 5 - 2
VeloeMinecraftLauncher/VeloeMinecraftLauncher.csproj

@@ -2,6 +2,7 @@
   <PropertyGroup>
     <OutputType>WinExe</OutputType>
     <TargetFramework>net7.0</TargetFramework>
+	<TieredCompilationQuickJit>false</TieredCompilationQuickJit>
     <Nullable>enable</Nullable>
     <!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
     <TrimMode>copyused</TrimMode>
@@ -9,8 +10,8 @@
     <DebugType>embedded</DebugType>
     <StartupObject>VeloeMinecraftLauncher.Program</StartupObject>
     <PlatformTarget>x64</PlatformTarget>
-    <AssemblyVersion>1.2.1.19</AssemblyVersion>
-    <FileVersion>1.2.1.19</FileVersion>
+    <AssemblyVersion>1.2.2.138</AssemblyVersion>
+    <FileVersion>1.2.2.138</FileVersion>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
     <NoWarn>NU1605</NoWarn>
@@ -37,12 +38,14 @@
     <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
     <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="0.10.18" />
     <PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" />
+    <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.0" />
     <PackageReference Include="ReactiveUI.Blazor" Version="18.4.1" />
     <PackageReference Include="ReactiveUI.Validation" Version="3.1.7" />
     <PackageReference Include="Serilog" Version="2.12.0" />
     <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
     <PackageReference Include="SerilogTraceListener" Version="3.2.0" />
+    <PackageReference Include="System.Net.Http" Version="4.3.4" />
     <PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
   </ItemGroup>
 </Project>

+ 30 - 18
VeloeMinecraftLauncher/ViewModels/MainWindowViewModel.cs

@@ -25,16 +25,17 @@ using Avalonia.Data;
 using System.Collections.Generic;
 using VeloeMinecraftLauncher.Models.Entity;
 using VeloeMinecraftLauncher.Models;
-using System.Collections.Specialized;
 using System.Linq;
+using System.Threading;
+using System.ComponentModel;
 
 namespace VeloeMinecraftLauncher.ViewModels;
 
 public class MainWindowViewModel : ViewModelBase
 {
     public MainWindowViewModel()
-    {           
-        Task.Run(() =>
+    {
+        Task.Run(async () =>
         {
             try
             {
@@ -74,7 +75,7 @@ public class MainWindowViewModel : ViewModelBase
             {
                 //check launcher update
                 _logger.Information("Checking launcher versions updates.");
-                _latestLauncherInfo = Downloader.DownloadAndDeserializeJsonData<LatestLauncherVersion>("https://files.veloe.link/launcher/update/versions.json");
+                _latestLauncherInfo = await Downloader.DownloadAndDeserializeJsonData<LatestLauncherVersion>("https://files.veloe.link/launcher/update/versions.json");
                 if (_latestLauncherInfo is not null && _latestLauncherInfo.Latest is not null)
                 {
                     _logger.Information("Launcher version on server: {0}", _latestLauncherInfo.Latest);
@@ -95,7 +96,7 @@ public class MainWindowViewModel : ViewModelBase
             }
             try
             {
-                var changelog = Downloader.DownloadAndDeserializeJsonData<List<Changelog>>("https://files.veloe.link/launcher/changelog.json");
+                var changelog = await Downloader.DownloadAndDeserializeJsonData<List<Changelog>>("https://files.veloe.link/launcher/changelog.json");
 
                 if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
                 {
@@ -131,17 +132,17 @@ public class MainWindowViewModel : ViewModelBase
             }
             try
             {
-              
+
                 _logger.Debug("Connecting to WebSoket");
-                    //conection to my servers 
+                //conection to my servers 
 
-                var serverNames = Downloader.DownloadAndDeserializeJsonData<List<string>>("https://files.veloe.link/launcher/servers.json");
+                var serverNames = await Downloader.DownloadAndDeserializeJsonData<List<string>>("https://files.veloe.link/launcher/servers.json");
 
                 _connection = new HubConnectionBuilder()
                     .WithUrl("https://monitor.veloe.link/hubs/data")
                     .WithAutomaticReconnect()
                     .Build();
-            
+
                 Func<Exception, Task> reconnecting = ex => Task.Run(() =>
                 {
                     _logger.Warning("Reconnecting to WebCoket...");
@@ -187,7 +188,7 @@ public class MainWindowViewModel : ViewModelBase
             try
             {
                 _logger.Debug("Checking modpacks updates...");
-                var modpacksInfo = Downloader.DownloadAndDeserializeJsonData<List<Modpack>>("https://files.veloe.link/launcher/modpacks.json");
+                var modpacksInfo = await Downloader.DownloadAndDeserializeJsonData<List<Modpack>>("https://files.veloe.link/launcher/modpacks.json");
                 var installedModpacks = //DownloadedVersions.Where(v => modpacksInfo.Select(m => m.Name).Contains(v.version)).Join(modpacksInfo, (v, m) => v.version == m.);
                     from downloadedVersion in DownloadedVersions
                     join modpack in modpacksInfo on downloadedVersion.version equals modpack.Name
@@ -247,13 +248,14 @@ public class MainWindowViewModel : ViewModelBase
     private string _downloadButton = "Download versions";
     private string _startButton = "Start Minecraft";
     private string _username = "";
-    private StringBuilder _consoleText = new StringBuilder();
+    private StringBuilder _consoleText = new();
     private int _downloadedIndex;
     private string _argumentsBox;
     private bool _isNoGameRunning = true;
     private bool _isUpdateAvailable = false;
     private string _settingsButton = "Settings";
     private string _startButtonOutput;
+    private CancellationTokenSource _tokenSource = new();
 
     ILogger _logger;
 
@@ -395,7 +397,7 @@ public class MainWindowViewModel : ViewModelBase
     public async void StartMinecraft()
     {
 #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
-        Task.Run(async() =>
+        await Task.Run(async() =>
         {
             try
             {
@@ -414,12 +416,15 @@ public class MainWindowViewModel : ViewModelBase
 
                     var versionJson = JsonSerializer.Deserialize<Entity.Version.Version>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
 
+                    if (versionJson is null)
+                        throw new ArgumentNullException(nameof(versionJson));
+
                     if (Settings.checkGameAssets)
                     {
                         TaskStatus result;
 
                         if (versionJson.InheritsFrom is null)
-                            result = Downloader.StartDownload(value => StartButtonOutput = value, value => IsNoGameRunning = value, value => { }, value => { }, versionJson).Result;
+                            result = await Downloader.StartDownload(value => StartButtonOutput = value, value => { } ,value => IsNoGameRunning = value, value => { }, versionJson, _tokenSource.Token);
                         else
                         {
                             using (StreamReader inheritsFromReader = new StreamReader(Settings.minecraftForlderPath + "versions/" + versionJson.InheritsFrom + "/" + versionJson.InheritsFrom + ".json"))
@@ -427,13 +432,14 @@ public class MainWindowViewModel : ViewModelBase
                                 string jsonInheritsFrom = inheritsFromReader.ReadToEnd();
                                 var inheritsFromJson = JsonSerializer.Deserialize<Entity.Version.Version>(jsonInheritsFrom, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
 
-                                result = Downloader.StartDownload(value => StartButtonOutput = value, value => IsNoGameRunning = value, value => { }, value => { }, inheritsFromJson).Result;
+                                result = await Downloader.StartDownload(value => StartButtonOutput = value, value => { } ,value => IsNoGameRunning = value, value => { }, inheritsFromJson, _tokenSource.Token);
                             }
                         }
 
                         if (result != TaskStatus.RanToCompletion)
                         {
                             IsNoGameRunning = true;
+                            StartButtonOutput = "Checking game files task faulted.";
                             return;
                         }
                     }
@@ -455,7 +461,7 @@ public class MainWindowViewModel : ViewModelBase
                                 {
                                     string jsonInheritsFrom = inheritsFromReader.ReadToEnd();
                                     var inheritsFromJson = JsonSerializer.Deserialize<Entity.Version.Version>(jsonInheritsFrom, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
-                                    if (inheritsFromJson.JavaVersion != null)
+                                    if (inheritsFromJson?.JavaVersion != null)
                                     {
                                         _logger.Debug("Java version required: {0}", inheritsFromJson.JavaVersion.MajorVersion);
                                         version = inheritsFromJson.JavaVersion.MajorVersion;
@@ -736,7 +742,7 @@ public class MainWindowViewModel : ViewModelBase
             updater.StartInfo.FileName += ".exe";
 
         if (!File.Exists(updater.StartInfo.FileName))
-            UpdateUpdater();
+            Task.Run(()=>UpdateUpdater()).Wait();
 
         if (File.Exists(updater.StartInfo.FileName))
         {
@@ -751,7 +757,7 @@ public class MainWindowViewModel : ViewModelBase
             OpenErrorWindow(new FileNotFoundException($"File {updater.StartInfo.FileName} not found!"));
     }
 
-    private void UpdateUpdater()
+    private async void UpdateUpdater()
     {
         try
         {
@@ -761,7 +767,7 @@ public class MainWindowViewModel : ViewModelBase
             if (File.Exists(fileName))
             {
                 FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(fileName);
-                LatestLauncherVersion latestLauncherVersion = Downloader.DownloadAndDeserializeJsonData<LatestLauncherVersion>("https://files.veloe.link/launcher/update/versions.json");
+                LatestLauncherVersion latestLauncherVersion = await Downloader.DownloadAndDeserializeJsonData<LatestLauncherVersion>("https://files.veloe.link/launcher/update/versions.json");
 
                 if (OperatingSystem.IsLinux())
                 {
@@ -882,4 +888,10 @@ public class MainWindowViewModel : ViewModelBase
 
         return panel;
     }
+
+    public void OnClosing(object sender, CancelEventArgs args)
+    {
+        _tokenSource.Cancel();
+        _tokenSource.Dispose();
+    }
 }

+ 154 - 132
VeloeMinecraftLauncher/ViewModels/VersionsDownloaderViewModel.cs

@@ -1,26 +1,22 @@
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Threading;
-using DynamicData;
+using DynamicData;
 using ReactiveUI;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.ComponentModel;
 using System.Linq;
-using System.Net;
-using System.Net.Http;
 using System.Text.Json;
+using System.Threading;
 using System.Threading.Tasks;
-using VeloeMinecraftLauncher.Entity.Version;
 using VeloeMinecraftLauncher.Entity.VersionManifest;
 using VeloeMinecraftLauncher.MinecraftLauncher;
 using VeloeMinecraftLauncher.Models.Entity;
-using VeloeMinecraftLauncher.Views;
 
 namespace VeloeMinecraftLauncher.ViewModels;
 
 public class VersionsDownloaderViewModel : ViewModelBase
 {
-    private string _installModpackButtonText = "Download Modpack";
+    private string _downloadButtonText = "Download";
     private bool _showOld = false;
     private bool _showSnaps = false;
     private bool _installFabric = false;
@@ -33,37 +29,45 @@ public class VersionsDownloaderViewModel : ViewModelBase
     private bool _installForgeOptifineVisible = false;
     private bool _downloadJava = false;
     private bool _isControlsEnabled = true;
-    private int _progress = 0;
-    private string _downloadingFileName;
+    private long _progress = 0;
+    private long _maxprogressvalue = 100;
+    private string _downloadingFileName = "No active downloads";
+    private string _tasksStatusLine = "No tasks started yet";
+    private CancellationTokenSource _tokenSource = new();
+    private CancellationTokenSource _filteredVersionTokenSource = new();
 
     Serilog.ILogger _logger;
 
     ObservableCollection<Entity.VersionManifest.Version> _filteredVersions;
     ObservableCollection<Modpack> _modpackVersions;
+    List<Entity.VersionManifest.Version> _modpackVersionsAsVersion;
     Entity.VersionManifest.Version _filteredVersion;
     Modpack _selectedModpack;
     VersionManifest _versionManifest;
 
     public VersionsDownloaderViewModel()
     {
+        IsControlsEnabled = false;
         try
         {
-            Task.Run(() =>
+            Task.Run(async () =>
             {
                 _logger = Settings.logger;
                 if (FilteredVersions is null)
                 {
                     FilteredVersions = new();
                 }
-                if (ModpackVersions is null)
+                if (_modpackVersions is null)
                 {
-                    ModpackVersions = new();
+                    _modpackVersions = new();
                 }
                 _logger.Debug("Getting versionManifest.json");
-                _versionManifest = Downloader.DownloadAndDeserializeJsonData<VersionManifest>("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json");
-                ModpackVersions.AddRange(Downloader.DownloadAndDeserializeJsonData<List<Modpack>>("https://files.veloe.link/launcher/modpacks.json"));
+                _versionManifest = await Downloader.DownloadAndDeserializeJsonData<VersionManifest>("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json");
+                _modpackVersions.AddRange(await Downloader.DownloadAndDeserializeJsonData<List<Modpack>>("https://files.veloe.link/launcher/modpacks.json"));
+                _modpackVersionsAsVersion = _modpackVersions.Select(v=> new Entity.VersionManifest.Version() { Id = v.Name, Type = "modpack", ComplianceLevel = v.Revision}).ToList();
                 _logger.Debug("Updating available versions to download.");
-                updateList();
+                UpdateList();
+                IsControlsEnabled = true;
             });
         }
         catch (Exception ex)
@@ -82,75 +86,84 @@ public class VersionsDownloaderViewModel : ViewModelBase
             InstallForge = false;
             InstallOptifine = false;
             InstallForgeOptifine = false;
-            try
-            {
-                using (var httpClient = new HttpClient())
-                {
 
-                    var response = httpClient.GetAsync(@$"https://files.veloe.link/launcher/forge/Forge{value.Id}/Forge{value.Id}.json").Result;
+            if (value is null)
+                return;
 
-                    if (response.IsSuccessStatusCode)
-                    {
-                        InstallForgeVisible = true;
-                        response = httpClient.GetAsync(@$"https://files.veloe.link/launcher/forge/Forge{value.Id}/Optifine{value.Id}.jar").Result;
-                        if (response.IsSuccessStatusCode)
-                            InstallForgeOptifineVisible = true;
-                    }
-                    else
+            if (value.Type == "modpack")
+            {
+                try
+                {
+                    if (System.IO.File.Exists(Settings.minecraftForlderPath + $"versions/{value.Id}/revision.json"))
                     {
-                        InstallForgeVisible = false;
-                        InstallForgeOptifineVisible = false;
+                        if (value.ComplianceLevel > JsonSerializer.Deserialize<int>(System.IO.File.ReadAllText(Settings.minecraftForlderPath + $"versions/{value.Id}/revision.json")))
+                            DownloadButtonText = "Update Modpack";
+                        else
+                            DownloadButtonText = "Reinstall Modpack";
                     }
-
-                        response = httpClient.GetAsync(@$"https://files.veloe.link/launcher/fabric/Fabric{value.Id}/Fabric{value.Id}.json").Result;
-
-                    if (response.IsSuccessStatusCode)
-                        InstallFabricVisible = true;
                     else
-                        _installFabricVisible = false;
-
-                    response = httpClient.GetAsync(@$"https://files.veloe.link/launcher/optifine/Optifine{value.Id}/Optifine{value.Id}.json").Result;
-
-                    if (response.IsSuccessStatusCode)
-                        InstallOptifineVisible = true;
+                        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
-                        InstallOptifineVisible = false;
-                
-
+                        DownloadButtonText = "Download Modpack";
+                }
+                catch (Exception)
+                {
+                    DownloadButtonText = "Update Modpack";
                 }
             }
-            catch (Exception ex)
+            else
             {
-                OpenErrorWindow(ex);
+                if (System.IO.File.Exists(Settings.minecraftForlderPath + $"versions/{value.Id}/{value.Id}.json"))
+                    DownloadButtonText = "Reinstall";
+                else
+                    DownloadButtonText = "Download";
             }
-        }
-    }
 
-    public Modpack SelectedModpack
-    {
-        get => _selectedModpack;
-        set
-        {
             try
             {
-                if (System.IO.File.Exists(Settings.minecraftForlderPath + $"versions/{value.Name}/revision.json"))
-                {               
-                    if (value.Revision > JsonSerializer.Deserialize<int>(System.IO.File.ReadAllText(Settings.minecraftForlderPath + $"versions/{value.Name}/revision.json")))
-                        InstallModpackButtonText = "Update Modpack";
-                    else
-                        InstallModpackButtonText = "Reinstall Modpack";              
-                }
-                else
-                    if (System.IO.Directory.Exists($"{Settings.minecraftForlderPath}versions/{value.Name}") && System.IO.File.Exists($"{Settings.minecraftForlderPath}versions/{value.Name}/{value.Name}.json"))
-                        InstallModpackButtonText = "Update Modpack";
-                    else
-                        InstallModpackButtonText = "Download Modpack";
+                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)
+            catch (Exception ex)
             {
-                InstallModpackButtonText = "Update Modpack";
+                OpenErrorWindow(ex);
             }
-            this.RaiseAndSetIfChanged(ref _selectedModpack, value); 
         }
     }
 
@@ -160,16 +173,10 @@ public class VersionsDownloaderViewModel : ViewModelBase
         set => this.RaiseAndSetIfChanged(ref _filteredVersions, value);
     }
 
-    public ObservableCollection<Models.Entity.Modpack> ModpackVersions
-    {
-        get => _modpackVersions;
-        set => this.RaiseAndSetIfChanged(ref _modpackVersions, value);
-    }
-
-    public string InstallModpackButtonText
+    public string DownloadButtonText
     {
-        get => _installModpackButtonText; 
-        set => this.RaiseAndSetIfChanged(ref _installModpackButtonText, value);
+        get => _downloadButtonText; 
+        set => this.RaiseAndSetIfChanged(ref _downloadButtonText, value);
     }
 
     public bool ShowOld
@@ -177,7 +184,7 @@ public class VersionsDownloaderViewModel : ViewModelBase
         get => _showOld;
         set { 
             this.RaiseAndSetIfChanged(ref _showOld, value);
-            updateList();
+            UpdateList();
         }
     }
 
@@ -186,22 +193,34 @@ public class VersionsDownloaderViewModel : ViewModelBase
         get => _showSnaps;
         set { 
             this.RaiseAndSetIfChanged(ref _showSnaps, value);
-            updateList();
+            UpdateList();
         }
     }
 
-    public int Progress
+    public long Progress
     {
         get => _progress;
         set => this.RaiseAndSetIfChanged(ref _progress, value);
     }
 
+    public long MaxProgressValue
+    {
+        get => _maxprogressvalue;
+        set => this.RaiseAndSetIfChanged(ref _maxprogressvalue, value);
+    }
+
     public string DownloadingFileName
     {
         get => _downloadingFileName;
         set => this.RaiseAndSetIfChanged(ref _downloadingFileName, value);
     }
 
+    public string TasksStatusLine
+    {
+        get => _tasksStatusLine;
+        set => this.RaiseAndSetIfChanged(ref _tasksStatusLine, value);
+    }
+
     public bool InstallForge
     {
         get => _installForge;
@@ -280,64 +299,58 @@ public class VersionsDownloaderViewModel : ViewModelBase
 
     public async Task OnStartBunttonClick()
     {
-#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
-        Task.Run(() => {
+        await Task.Run(async () => {
 
             if (FilteredVersion is null)
                 return TaskStatus.Faulted;
 
-            _logger.Debug("Downloading {0}.json", FilteredVersion.Id);
-            DownloadingFileName = $"{FilteredVersion.Id}.json";
-            var versionJson = Downloader.DownloadAndDeserializeJsonData<Entity.Version.Version>(FilteredVersion.Url, Settings.minecraftForlderPath + "versions/" + FilteredVersion.Id + "/", FilteredVersion.Id + ".json");
-
-            if (versionJson is not null)
-                Downloader.StartDownload(
-                  value => DownloadingFileName = value,
-                  value => IsControlsEnabled = value,
-                  value => value.DownloadProgressChanged += WebClient_DownloadProgressChanged,
-                  value => value.DownloadProgressChanged -= WebClient_DownloadProgressChanged,
-                  versionJson,
-                  DownloadJava,
-                  InstallForge,
-                  InstallOptifine,
-                  InstallForgeOptifine,
-                  InstallFabric);
+            if (FilteredVersion.Type == "modpack")
+            {
+                _selectedModpack = _modpackVersions.Where(m => m.Name == FilteredVersion.Id).FirstOrDefault();
+                if (_selectedModpack is null)
+                    return TaskStatus.Faulted;
+
+                if (FilteredVersions.Where(x => x.Id == _selectedModpack.Version).FirstOrDefault() is null)
+                    return TaskStatus.Faulted;
+
+                await Task.Run(async () => await Downloader.StartDownloadModpack(
+                    value => DownloadingFileName = value,
+                    value => TasksStatusLine = value,
+                    value => IsControlsEnabled = value,
+                    value => Progress = value,
+                    FilteredVersions,
+                    _selectedModpack,
+                    _tokenSource.Token,
+                    DownloadJava,
+                    InstallFabric = _selectedModpack.Fabric,
+                    InstallForge = _selectedModpack.Forge,
+                    InstallOptifine = _selectedModpack.Optifine,
+                    InstallForgeOptifine = _selectedModpack.ForgeOptifine));
+            }
+            else
+            {
+                _logger.Debug("Downloading {0}.json", FilteredVersion.Id);
+                DownloadingFileName = $"{FilteredVersion.Id}.json";
+                var versionJson = await Downloader.DownloadAndDeserializeJsonData<Entity.Version.Version>(FilteredVersion.Url, Settings.minecraftForlderPath + "versions/" + FilteredVersion.Id + "/", FilteredVersion.Id + ".json");
+
+                if (versionJson is not null)
+                    await Downloader.StartDownload(
+                      value => DownloadingFileName = value,
+                      value => TasksStatusLine = value,
+                      value => IsControlsEnabled = value,
+                      value => Progress = value,
+                      versionJson,
+                      _tokenSource.Token,
+                      DownloadJava,
+                      InstallForge,
+                      InstallOptifine,
+                      InstallForgeOptifine,
+                      InstallFabric);
+            }
         return TaskStatus.RanToCompletion; });
-#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
-    }
-  
-    public async void OnStartModpackBunttonClick()
-    {
-        if (SelectedModpack is null)
-            return;
-
-        FilteredVersion = FilteredVersions.Where(x => x.Id == SelectedModpack.Version).FirstOrDefault();
-
-        if (FilteredVersion is null)
-            return;
-
-#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
-        Task.Run(() => Downloader.StartDownloadModpack(
-            value => DownloadingFileName = value,
-            value => IsControlsEnabled = value,
-            value => value.DownloadProgressChanged += WebClient_DownloadProgressChanged,
-            value => value.DownloadProgressChanged -= WebClient_DownloadProgressChanged,
-            FilteredVersions,
-            SelectedModpack,
-            DownloadJava,
-            InstallFabric = SelectedModpack.Fabric,
-            InstallForge = SelectedModpack.Forge,
-            InstallOptifine = SelectedModpack.Optifine,
-            InstallForgeOptifine = SelectedModpack.ForgeOptifine));
-#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
-    }
-
-    private void WebClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
-    {
-        Progress = e.ProgressPercentage;
     }
 
-    public void updateList()
+    public void UpdateList()
     {
         try
         {
@@ -346,6 +359,7 @@ public class VersionsDownloaderViewModel : ViewModelBase
             if (_versionManifest.Versions is null)
                 return;
 
+            FilteredVersions.AddRange(_modpackVersionsAsVersion);
             FilteredVersions.AddRange(_versionManifest.Versions.Where(version => ShowSnaps && version.Type == "snapshot" || ShowOld && version.Type is ("old_alpha" or "old_beta") || version.Type == "release"));
         }
         catch (Exception ex)
@@ -353,4 +367,12 @@ public class VersionsDownloaderViewModel : ViewModelBase
             OpenErrorWindow(ex);
         }
     }
+
+    public void OnClosing(object sender, CancelEventArgs args)
+    {
+        _tokenSource.Cancel();
+        _tokenSource.Dispose();
+        _filteredVersionTokenSource.Cancel();
+        _filteredVersionTokenSource.Dispose();
+    }
 }

+ 2 - 1
VeloeMinecraftLauncher/Views/MainWindow.axaml

@@ -15,7 +15,8 @@
 		CanResize="False"
 		ExtendClientAreaToDecorationsHint="True"
 		ExtendClientAreaChromeHints="NoChrome"
-		ExtendClientAreaTitleBarHeightHint="-1">
+		ExtendClientAreaTitleBarHeightHint="-1"
+		Closing="MainWindow_Closing">
 
 	<Window.Background>
 		<ImageBrush

+ 9 - 0
VeloeMinecraftLauncher/Views/MainWindow.axaml.cs

@@ -1,4 +1,5 @@
 using Avalonia.Controls;
+using VeloeMinecraftLauncher.ViewModels;
 
 namespace VeloeMinecraftLauncher.Views
 {
@@ -8,5 +9,13 @@ namespace VeloeMinecraftLauncher.Views
         {
             InitializeComponent();
         }
+
+        private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+        {
+            if (DataContext is MainWindowViewModel model)
+            {
+                model.OnClosing(sender, e);
+            }
+        }
     }
 }

+ 82 - 20
VeloeMinecraftLauncher/Views/VersionsDownloader.axaml

@@ -4,10 +4,12 @@
 		xmlns:vm="using:VeloeMinecraftLauncher.ViewModels"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+		xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
+             xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions"
 		xmlns:titlebars="using:VeloeMinecraftLauncher.Views.TitleBar"
-        mc:Ignorable="d" d:DesignWidth="250" d:DesignHeight="450"
-		Width="250" Height="450"
-		MaxWidth="250" MaxHeight="450"
+        mc:Ignorable="d" d:DesignWidth="350" d:DesignHeight="450"
+		Width="350" Height="450"
+		MaxWidth="350" MaxHeight="450"
         x:Class="VeloeMinecraftLauncher.Views.VersionsDownloader"
         Icon="/Assets/avalonia-logo.ico"
         Title="Versions Downloader"
@@ -17,7 +19,7 @@
 		ExtendClientAreaToDecorationsHint="True"
 		ExtendClientAreaChromeHints="NoChrome"
 		ExtendClientAreaTitleBarHeightHint="-1"
-		>
+		Closing="VersionDownloader_Closing">
 	
 	<Design.DataContext>
 		<vm:VersionsDownloaderViewModel/>
@@ -41,21 +43,81 @@
 			  TitleText="Versions"
 			  DockPanel.Dock="Top">
 			</titlebars:TitleBarWindow>
-			<StackPanel Margin="10" Spacing="5">
-				<ComboBox Items="{Binding FilteredVersions}" PlaceholderText="Select version" SelectedItem="{Binding FilteredVersion}" IsEnabled="{Binding IsControlsEnabled}" HorizontalAlignment="Stretch"></ComboBox>
-				<CheckBox IsChecked="{Binding ShowOld}" IsEnabled="{Binding IsControlsEnabled}">Show old</CheckBox>
-				<CheckBox IsChecked="{Binding ShowSnaps}" IsEnabled="{Binding IsControlsEnabled}">Show snapshots</CheckBox>
-				<CheckBox IsChecked="{Binding DownloadJava}" IsEnabled="{Binding IsControlsEnabled}">Download Java</CheckBox>
-				<CheckBox IsChecked="{Binding InstallFabric}" IsVisible="{Binding InstallFabricVisible}" IsEnabled="{Binding IsControlsEnabled}">Install Fabric</CheckBox>
-				<CheckBox IsChecked="{Binding InstallForge}" IsVisible="{Binding InstallForgeVisible}" IsEnabled="{Binding IsControlsEnabled}">Install Forge</CheckBox>
-				<CheckBox IsChecked="{Binding InstallForgeOptifine}" IsVisible="{Binding InstallForgeOptifineVisible}" IsEnabled="{Binding IsControlsEnabled}">Install Optifine (Mod)</CheckBox>
-				<CheckBox IsChecked="{Binding InstallOptifine}" IsVisible="{Binding InstallOptifineVisible}" IsEnabled="{Binding IsControlsEnabled}">Install Optifine (Vanilla)</CheckBox>
-				<Button Content="Download" HorizontalAlignment="Center" Command="{Binding OnStartBunttonClick}" IsEnabled="{Binding IsControlsEnabled}"></Button>
-				<ComboBox Items="{Binding ModpackVersions}" PlaceholderText="Select pack" SelectedItem="{Binding SelectedModpack}" IsEnabled="{Binding IsControlsEnabled}" HorizontalAlignment="Stretch"></ComboBox>
-				<Button Content="{Binding InstallModpackButtonText}" HorizontalAlignment="Center" Command="{Binding OnStartModpackBunttonClick}" IsEnabled="{Binding IsControlsEnabled}"></Button>
-				<ProgressBar Value="{Binding Progress}" ShowProgressText="true"></ProgressBar>
-				<TextBlock Text="{Binding DownloadingFileName}"></TextBlock>
-			</StackPanel>
+			<Grid Margin="10" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,*,Auto,Auto,Auto,Auto">
+				<ComboBox 
+					Items="{Binding FilteredVersions}" 
+					PlaceholderText="Select version" 
+					SelectedItem="{Binding FilteredVersion}" 
+					IsEnabled="{Binding IsControlsEnabled}" 
+					HorizontalAlignment="Stretch"
+					Grid.Row="0" Grid.Column="0"/>
+				<CheckBox 
+					IsChecked="{Binding ShowOld}" 
+					IsEnabled="{Binding IsControlsEnabled}"
+					Margin="0,10,0,0"
+					Grid.Row="1" Grid.Column="0"
+					Content="Show old"/>
+				<CheckBox 
+					IsChecked="{Binding ShowSnaps}" 
+					IsEnabled="{Binding IsControlsEnabled}"
+					Margin="0,5,0,0"
+					Grid.Row="2" Grid.Column="0"
+					Content="Show snapshots"/>
+				<CheckBox 
+					IsChecked="{Binding DownloadJava}" 
+					IsEnabled="{Binding IsControlsEnabled}"
+					Margin="0,5,0,0"
+					Grid.Row="3" Grid.Column="0"
+					Content="Download Java"/>
+				<CheckBox 
+					IsChecked="{Binding InstallFabric}" 
+					IsVisible="{Binding InstallFabricVisible}" 
+					IsEnabled="{Binding IsControlsEnabled}"
+					Margin="0,5,0,0"
+					Grid.Row="4" Grid.Column="0"
+					Content="Install Fabric"/>
+				<CheckBox 
+					IsChecked="{Binding InstallForge}" 
+					IsVisible="{Binding InstallForgeVisible}" 
+					IsEnabled="{Binding IsControlsEnabled}"
+					Margin="0,5,0,0"
+					Grid.Row="5" Grid.Column="0"
+					Content="Install Forge"/>
+				<CheckBox 
+					IsChecked="{Binding InstallForgeOptifine}" 
+					IsVisible="{Binding InstallForgeOptifineVisible}" 
+					IsEnabled="{Binding IsControlsEnabled}"
+					Margin="0,5,0,0"
+					Grid.Row="6" Grid.Column="0"
+					Content="Install Optifine (Mod)"/>
+				<CheckBox 
+					IsChecked="{Binding InstallOptifine}" 
+					IsVisible="{Binding InstallOptifineVisible}" 
+					IsEnabled="{Binding IsControlsEnabled}"
+					Margin="0,5,0,0"
+					Grid.Row="7" Grid.Column="0"
+					Content="Install Optifine (Vanilla)"/>
+				<Button 
+					Content="{Binding DownloadButtonText}" 
+					HorizontalAlignment="Center" 
+					Command="{Binding OnStartBunttonClick}" 
+					IsEnabled="{Binding IsControlsEnabled}"
+					Margin="0,10,0,0"
+					Grid.Row="9" Grid.Column="0"/>
+				<ProgressBar 
+					Value="{Binding Progress}" 
+					Maximum="{Binding MaxProgressValue}" 
+					ShowProgressText="true"
+					Margin="0,10,0,0"
+					Grid.Row="10" Grid.Column="0"/>
+				<TextBlock 
+					Text="{Binding TasksStatusLine}"
+					Margin="0,10,0,0"
+					Grid.Row="11" Grid.Column="0"/>
+				<TextBlock 
+					Text="{Binding DownloadingFileName}"
+					Grid.Row="12" Grid.Column="0"/>
+			</Grid>
 		</DockPanel>
-		</Panel>
+	</Panel>
 </Window>

+ 9 - 0
VeloeMinecraftLauncher/Views/VersionsDownloader.axaml.cs

@@ -1,4 +1,5 @@
 using Avalonia.Controls;
+using VeloeMinecraftLauncher.ViewModels;
 
 namespace VeloeMinecraftLauncher.Views
 {
@@ -8,5 +9,13 @@ namespace VeloeMinecraftLauncher.Views
         {
             InitializeComponent();
         }
+
+        private void VersionDownloader_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+        {
+            if (DataContext is VersionsDownloaderViewModel model)
+            {
+                model.OnClosing(sender,e);
+            }
+        }
     }
 }