diff --git a/Knossos.NET/App.axaml b/Knossos.NET/App.axaml index 86cf03f5..07fbaa99 100644 --- a/Knossos.NET/App.axaml +++ b/Knossos.NET/App.axaml @@ -15,6 +15,7 @@ + diff --git a/Knossos.NET/Knossos.NET.csproj b/Knossos.NET/Knossos.NET.csproj index cec9d8d6..501faaaf 100644 --- a/Knossos.NET/Knossos.NET.csproj +++ b/Knossos.NET/Knossos.NET.csproj @@ -70,6 +70,7 @@ + @@ -90,6 +91,9 @@ DeveloperModsView.axaml + + DevModAdvancedUploadView.axaml + MessageBox.axaml diff --git a/Knossos.NET/Models/Mod.cs b/Knossos.NET/Models/Mod.cs index 77c24543..9eab44c1 100644 --- a/Knossos.NET/Models/Mod.cs +++ b/Knossos.NET/Models/Mod.cs @@ -779,6 +779,7 @@ public async Task LoadFulLNebulaData() /// /// To use with the List .Sort() + /// Retuns a list that is sort from older to newer /// /// /// @@ -788,6 +789,18 @@ public static int CompareVersion(Mod mod1, Mod mod2) return SemanticVersion.Compare(mod1.version, mod2.version); } + /// + /// To use with the List .Sort() + /// Retuns a list that is sort from newer to older + /// + /// + /// + public static int CompareVersionNewerToOlder(Mod mod1, Mod mod2) + { + //inverted + return SemanticVersion.Compare(mod2.version, mod1.version); + } + /// /// Compares two mods and determines if the metadata is different /// Full data must be loaded on both mods for this to work properly diff --git a/Knossos.NET/ViewModels/TaskViewModel.cs b/Knossos.NET/ViewModels/TaskViewModel.cs index 9daf2131..bcd7725c 100644 --- a/Knossos.NET/ViewModels/TaskViewModel.cs +++ b/Knossos.NET/ViewModels/TaskViewModel.cs @@ -436,7 +436,10 @@ public async void CreateModVersion(Mod oldMod, string newVersion, Action hackCal /// /// /// - public async void UploadModVersion(Mod mod, bool isNewMod, bool metadataonly = false) + /// + /// + /// + public async void UploadModVersion(Mod mod, bool isNewMod, bool metadataonly = false, int parallelCompression = 1, int parallelUploads = 1, List? advData = null ) { using (var cancelSource = new CancellationTokenSource()) { @@ -446,7 +449,7 @@ public async void UploadModVersion(Mod mod, bool isNewMod, bool metadataonly = f TaskList.Add(newTask); taskQueue.Enqueue(newTask); }); - await newTask.UploadModVersion(mod, isNewMod, metadataonly, cancelSource).ConfigureAwait(false); + await newTask.UploadModVersion(mod, isNewMod, metadataonly, cancelSource, parallelCompression, parallelUploads, advData).ConfigureAwait(false); } } diff --git a/Knossos.NET/ViewModels/Templates/DevModVersionsViewModel.cs b/Knossos.NET/ViewModels/Templates/DevModVersionsViewModel.cs index bd9aa9bb..4b95cf28 100644 --- a/Knossos.NET/ViewModels/Templates/DevModVersionsViewModel.cs +++ b/Knossos.NET/ViewModels/Templates/DevModVersionsViewModel.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; namespace Knossos.NET.ViewModels { @@ -217,7 +218,44 @@ public void HackUpdateModList() Mods = Mods.ToList(); } + internal async void UploadModAdvanced() + { + var mod = editor?.ActiveVersion; + if (mod != null) + { + if (!mod.inNebula) + { + if (mod.type == ModType.mod || mod.type == ModType.tc) + { + var dialog = new DevModAdvancedUploadView(); + dialog.DataContext = new DevModAdvancedUploadViewModel(mod, dialog, this); + await dialog.ShowDialog(MainWindow.instance!); + } + else + { + _ = MessageBox.Show(MainWindow.instance!, "Advanced uploading is only available for Mods and TCs.", "Unsupported for type: " + mod.type, MessageBox.MessageBoxButtons.OK); + } + } + else + { + _ = MessageBox.Show(MainWindow.instance!, "This mod version is already uploaded to nebula, if you want to update the metadata use the basic upload", "Mod already uploaded", MessageBox.MessageBoxButtons.OK); + } + } + } + + //For UI button internal async void UploadMod() + { + await UploadProcess(); + } + + //for the advanced upload window + public void AdvancedUpload(List advData, int parallelCompression, int parallelUploads) + { + _ = UploadProcess(advData, parallelCompression, parallelUploads); + } + + private async Task UploadProcess(List? advData = null, int parallelCompression = 1, int parallelUploads = 1) { /* * Pre-Upload Checks: @@ -502,7 +540,7 @@ await MessageBox.Show(MainWindow.instance!, "Your mod depends on mod id: " + dep //Basic check finish, create task ButtonsEnabled = true; - TaskViewModel.Instance!.UploadModVersion(mod, isNewMod, metaUpdOnly); + TaskViewModel.Instance!.UploadModVersion(mod, isNewMod, metaUpdOnly, parallelCompression, parallelUploads, advData); } } catch(Exception ex) diff --git a/Knossos.NET/ViewModels/Templates/TaskItemViewModel.cs b/Knossos.NET/ViewModels/Templates/TaskItemViewModel.cs index 1c2a5cb4..44786f3f 100644 --- a/Knossos.NET/ViewModels/Templates/TaskItemViewModel.cs +++ b/Knossos.NET/ViewModels/Templates/TaskItemViewModel.cs @@ -1522,7 +1522,7 @@ private async Task PrepareModPkg(ModPackage pkg, string modFullPath, Cance } } - public async Task UploadModVersion(Mod mod, bool isNewMod, bool metaOnly, CancellationTokenSource? cancelSource = null) + public async Task UploadModVersion(Mod mod, bool isNewMod, bool metaOnly, CancellationTokenSource? cancelSource = null, int parallelCompression = 1, int parallelUploads = 1, List? advData = null ) { try { @@ -1607,13 +1607,42 @@ public async Task UploadModVersion(Mod mod, bool isNewMod, bool metaOnly, Info = "Prepare Packages"; Directory.CreateDirectory(mod.fullPath + Path.DirectorySeparatorChar + "kn_upload"); //Prepare packages, update data on mod - await Parallel.ForEachAsync(mod.packages, new ParallelOptions { MaxDegreeOfParallelism = Knossos.globalSettings.compressionMaxParallelism }, async (pkg, token) => + await Parallel.ForEachAsync(mod.packages, new ParallelOptions { MaxDegreeOfParallelism = parallelCompression }, async (pkg, token) => { - if (mod.type != ModType.mod && mod.type != ModType.tc) //Just to be sure - pkg.isVp = false; - var newTask = new TaskItemViewModel(); - await Dispatcher.UIThread.InvokeAsync(() => TaskList.Insert(0, newTask)); - await newTask.PrepareModPkg(pkg, mod.fullPath, cancellationTokenSource); + bool skipPkg = false; + //We should skip this? + if(advData != null) + { + var advDataPkg = advData.FirstOrDefault(p => p.packageInNebula != null && p.packageInNebula!.name == pkg.name); + if (advDataPkg != null && !advDataPkg.Upload) + { + var uploadedPkg = advDataPkg.packageInNebula; + if (uploadedPkg != null) + { + pkg.notes = uploadedPkg.notes; + pkg.isVp = uploadedPkg.isVp; + pkg.status = uploadedPkg.status; + pkg.filelist = uploadedPkg.filelist; + pkg.files = uploadedPkg.files; + pkg.dependencies = uploadedPkg.dependencies; + pkg.environment = uploadedPkg.environment; + pkg.executables = uploadedPkg.executables; + pkg.folder = uploadedPkg.folder; + pkg.checkNotes = uploadedPkg.checkNotes; + pkg.files?.ForEach(f => f.urls = null); //Cant send urls to Nebula or it gets rejected + skipPkg = true; + Log.Add(Log.LogSeverity.Information, "TaskItemViewModel.UploadModVersion()", "Skipping package preparation for :" + pkg.name + ". Data was loaded from Nebula."); + } + } + } + if (!skipPkg) + { + if (mod.type != ModType.mod && mod.type != ModType.tc) //Just to be sure + pkg.isVp = false; + var newTask = new TaskItemViewModel(); + await Dispatcher.UIThread.InvokeAsync(() => TaskList.Insert(0, newTask)); + await newTask.PrepareModPkg(pkg, mod.fullPath, cancellationTokenSource); + } ProgressCurrent++; if (cancellationTokenSource.IsCancellationRequested) throw new TaskCanceledException(); @@ -1621,11 +1650,29 @@ public async Task UploadModVersion(Mod mod, bool isNewMod, bool metaOnly, Info = "Upload Packages"; //Upload Packages - await Parallel.ForEachAsync(mod.packages, new ParallelOptions { MaxDegreeOfParallelism = 1 }, async (pkg, token) => + await Parallel.ForEachAsync(mod.packages, new ParallelOptions { MaxDegreeOfParallelism = parallelUploads }, async (pkg, token) => { - var newTask = new TaskItemViewModel(); - await Dispatcher.UIThread.InvokeAsync(() => TaskList.Insert(0, newTask)); - await newTask.UploadModPkg(pkg, mod.fullPath, cancellationTokenSource); + bool skipPkg = false; + //We should skip this? + if (advData != null) + { + var advDataPkg = advData.FirstOrDefault(p => p.packageInNebula != null && p.packageInNebula!.name == pkg.name); + if (advDataPkg != null && !advDataPkg.Upload) + { + var uploadedPkg = advDataPkg.packageInNebula; + if (uploadedPkg != null) + { + skipPkg = true; + Log.Add(Log.LogSeverity.Information, "TaskItemViewModel.UploadModVersion()", "Skipping package upload for :" + pkg.name + ". Used the one in Nebula instead, file hash: " + advDataPkg.CustomHash ); + } + } + } + if (!skipPkg) + { + var newTask = new TaskItemViewModel(); + await Dispatcher.UIThread.InvokeAsync(() => TaskList.Insert(0, newTask)); + await newTask.UploadModPkg(pkg, mod.fullPath, cancellationTokenSource); + } ProgressCurrent++; if (cancellationTokenSource.IsCancellationRequested) throw new TaskCanceledException(); diff --git a/Knossos.NET/ViewModels/Windows/DevModAdvancedUploadViewModel.cs b/Knossos.NET/ViewModels/Windows/DevModAdvancedUploadViewModel.cs new file mode 100644 index 00000000..d73db93c --- /dev/null +++ b/Knossos.NET/ViewModels/Windows/DevModAdvancedUploadViewModel.cs @@ -0,0 +1,278 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using Knossos.NET.Models; +using Knossos.NET.Views; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using static System.Net.Mime.MediaTypeNames; + +namespace Knossos.NET.ViewModels +{ + public partial class DevModAdvancedUploadData : ObservableObject + { + [ObservableProperty] + internal List otherVersions = new List(); + + internal int otherVersionsSelectedIndex = 0; + internal int OtherVersionsSelectedIndex + { + get { return otherVersionsSelectedIndex; } + set + { + if (otherVersionsSelectedIndex != value) + { + this.SetProperty(ref otherVersionsSelectedIndex, value); + if (value == 0 ) // Auto + { + CustomHash = ""; + } + else + { + if(package == null) + { + CustomHash = "Error: Local package was null, this should not happen."; + return; + } + try + { + //Get package data from nebula and let the cache do its thing + if (packageInNebula == null) + { + if (OtherVersions[value].DataContext == null) + { + CustomHash = "Error: Datacontext was null"; + return; + } + CustomHash = "Loading..."; + var m = (Mod)OtherVersions[value].DataContext!; + Task.Run(async () => + { + try + { + var modTask = await Nebula.GetModData(mod!.id, m.version); + if (modTask == null) + { + CustomHash = "Error: Unable to get data from Nebula"; + return; + } + packageInNebula = modTask.packages.FirstOrDefault(p => p.name == package.name); + if (packageInNebula == null) + { + CustomHash = "Error: Package was not found on selected version, or Nebula error."; + return; + } + CustomHash = packageInNebula!.files!.FirstOrDefault()!.checksum![1].ToString(); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "DevModAdvancedUploadData(OtherVersionsSelectedIndex.Setter2)", ex); + CustomHash = "Error in getting data from Nebula"; + }; + }); + } + else + { + CustomHash = packageInNebula!.files!.FirstOrDefault()!.checksum![1].ToString(); + } + } + catch (Exception ex) + { + CustomHash = "Error getting hash"; + Log.Add(Log.LogSeverity.Error, "DevModAdvancedUploadData(OtherVersionsSelectedIndex.Setter3)", ex); + } + } + } + } + } + + internal bool upload = true; + internal bool Upload + { + get { return upload; } + set + { + if (upload != value) + { + this.SetProperty(ref upload, value); + try + { + if (value == false) + { + //Enable all Versions listed in the combobox + OtherVersions.ForEach(o => o.IsEnabled = true); + OtherVersions[0].IsEnabled = false; // Disable "auto" + CustomHash = ""; + //Select what should be the latest version of a mod id + OtherVersionsSelectedIndex = OtherVersions.Count() - 1; + } + else + { + //Disable all versions listed in the combobox + OtherVersions.ForEach(o => o.IsEnabled = false); + OtherVersions[0].IsEnabled = true; //Enable Auto + //Select Auto + OtherVersionsSelectedIndex = 0; + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "DevModAdvancedUploadData(Upload.Setter)", ex); + } + } + } + } + + [ObservableProperty] + internal string customHash = ""; + + public ModPackage? package; + public ModPackage? packageInNebula = null; + private Mod? mod; + + public string PackageName + { + get + { + if (package != null) + return package.name; + else + return "package is null"; + } + } + + + public DevModAdvancedUploadData(ModPackage package, Mod mod, List allVersionsOfMod) + { + this.package = package; + this.mod = mod; + + //Fill Get Hash Combobox + var auto = new ComboBoxItem(); + auto.Content = "Auto"; + auto.IsEnabled = true; + OtherVersions.Add(auto); + + foreach (var o in allVersionsOfMod) + { + if (o.version != mod.version) + { + //List version in the combobox + var item = new ComboBoxItem(); + item.DataContext = o; + item.Content = o.version.ToString(); + item.IsEnabled = false; + item.Foreground = o.isPrivate ? Brushes.Red : Brushes.Cyan; + OtherVersions.Add(item); + } + } + OtherVersionsSelectedIndex = 0; + } + } + + /********************************************************************************/ + + public partial class DevModAdvancedUploadViewModel : ViewModelBase + { + [ObservableProperty] + internal string title = string.Empty; + [ObservableProperty] + internal bool loading = true; + [ObservableProperty] + internal int parallelCompressing = 1; + [ObservableProperty] + internal int parallelUploads = 1; + [ObservableProperty] + internal List modPackagesData = new List(); + + private Mod? uploadMod = null; + private DevModAdvancedUploadView? dialog = null; + private DevModVersionsViewModel? versionsViewModel = null; + public DevModAdvancedUploadViewModel() + { + } + + public DevModAdvancedUploadViewModel(Mod mod, DevModAdvancedUploadView? dialog, DevModVersionsViewModel? versionsViewModel) + { + this.dialog = dialog; + this.versionsViewModel = versionsViewModel; + Title = "Advanced Nebula Upload: " + mod; + uploadMod = mod; + _ = Task.Run(() => { LazyLoading(); }); + this.versionsViewModel = versionsViewModel; + } + + private async void LazyLoading() + { + if (uploadMod != null && uploadMod.packages != null) + { + var allVersionsOfThisMod = await Nebula.GetAllModsWithID(uploadMod.id); + allVersionsOfThisMod.Sort(Mod.CompareVersionNewerToOlder); + foreach (var package in uploadMod.packages) + { + Dispatcher.UIThread.Invoke(() => + { + var data = new DevModAdvancedUploadData(package, uploadMod, allVersionsOfThisMod); + ModPackagesData.Add(data); + }); + } + } + Loading = false; + } + + internal async void UploadMod() + { + //We have to check that all packages we arent going to upload has a valid sha256 and that they had been uploaded to nebula + //Basic check + foreach (var data in ModPackagesData) + { + if (!data.upload) + { + if (String.IsNullOrWhiteSpace(data.CustomHash) || data.CustomHash.Length == 0) + { + _ = MessageBox.Show(MainWindow.instance!, "Package: " + data.PackageName + " is not set to upload but it lacks a defined sha256. Operation cancelled.", "Missing hash data", MessageBox.MessageBoxButtons.OK); + return; + } + else + { + //ensure it is in lowercase + data.CustomHash = data.CustomHash.ToLower(); + } + + if(data.packageInNebula == null) + { + _ = MessageBox.Show(MainWindow.instance!, "Package: " + data.PackageName + " is not set to upload but it lacks the package data from Nebula. Operation cancelled.", "Missing package data", MessageBox.MessageBoxButtons.OK); + return; + } + } + } + + //Check with nebula if the file is uploaded + //Disabled, no need since we are getting the file hash from nebula the file HAS to be uploaded. + /* + foreach (var data in ModPackagesData) + { + if (!data.upload) + { + if(await Nebula.IsFileUploaded(data.CustomHash) == false) + { + _ = MessageBox.Show(MainWindow.instance!, "The provided sha256 hash for package: " + data.PackageName + " is not valid or not uploaded to Nebula, operation cancelled. Passed hash:" + data.CustomHash, "File hash is not uploaded to nebula (or it is incorrect)", MessageBox.MessageBoxButtons.OK); + Log.Add(Log.LogSeverity.Error,"DevModAdvancedUploadViewModel.UploadMod", "The provided sha256 hash for package: " + data.PackageName + " is not valid or not uploaded to Nebula, operation cancelled. Passed hash:"+data.CustomHash); + return; + } + await Task.Delay(1000); + } + } + */ + + //Send to upload and close window + await Dispatcher.UIThread.InvokeAsync( ()=> { + versionsViewModel?.AdvancedUpload(ModPackagesData, ParallelCompressing, ParallelUploads); + dialog?.Close(); + }); + } + } +} diff --git a/Knossos.NET/Views/Templates/DevModVersionsView.axaml b/Knossos.NET/Views/Templates/DevModVersionsView.axaml index 0d497f37..b52a072c 100644 --- a/Knossos.NET/Views/Templates/DevModVersionsView.axaml +++ b/Knossos.NET/Views/Templates/DevModVersionsView.axaml @@ -48,9 +48,10 @@ - + - + + + + + diff --git a/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml.cs b/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml.cs new file mode 100644 index 00000000..3b8cd273 --- /dev/null +++ b/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Knossos.NET.Views; + +public partial class DevModAdvancedUploadView : Window +{ + public DevModAdvancedUploadView() + { + InitializeComponent(); + } +} \ No newline at end of file