From ad8210ac88aecb2cdbf5a27efd19a3fdda9516bd Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sat, 25 Jan 2025 16:33:03 -0300 Subject: [PATCH 01/13] Sort and filtering rework, and add basic mod tag support --- Knossos.NET/AppStyles.axaml | 5 + Knossos.NET/Classes/Knossos.cs | 15 +- Knossos.NET/Classes/ModTags.cs | 251 ++++++++++++ Knossos.NET/Knossos.NET.csproj | 1 + Knossos.NET/Models/GlobalSettings.cs | 6 +- Knossos.NET/Models/Mod.cs | 67 ++++ Knossos.NET/ViewModels/ModListViewModel.cs | 236 ++++++------ .../ViewModels/NebulaModListViewModel.cs | 363 ++++++------------ .../ViewModels/Templates/ModCardViewModel.cs | 9 +- .../Templates/NebulaModCardViewModel.cs | 62 ++- .../ViewModels/Windows/MainWindowViewModel.cs | 53 +-- Knossos.NET/Views/ModListView.axaml | 43 ++- Knossos.NET/Views/ModListView.axaml.cs | 168 ++++++++ Knossos.NET/Views/NebulaModListView.axaml | 45 ++- Knossos.NET/Views/NebulaModListView.axaml.cs | 166 ++++++++ 15 files changed, 1023 insertions(+), 467 deletions(-) create mode 100644 Knossos.NET/Classes/ModTags.cs diff --git a/Knossos.NET/AppStyles.axaml b/Knossos.NET/AppStyles.axaml index ce7ac926..aea38f22 100644 --- a/Knossos.NET/AppStyles.axaml +++ b/Knossos.NET/AppStyles.axaml @@ -80,6 +80,7 @@ M4.21157,12.7326 C3.9244,13.0312 3.93361,13.5059 4.23213,13.7931 C4.53064,14.0803 5.00543,14.0711 5.29259,13.7725 L13.2521,5.49831 L13.2521,24.2511 C13.2521,24.6653 13.5879,25.0011 14.0021,25.0011 C14.4163,25.0011 14.7521,24.6653 14.7521,24.2511 L14.7521,5.4993 L22.7106,13.7725 C22.9978,14.0711 23.4726,14.0803 23.7711,13.7931 C24.0696,13.5059 24.0788,13.0312 23.7916,12.7326 L14.7223,3.30466 C14.3289,2.89568 13.6743,2.89568 13.2809,3.30466 L4.21157,12.7326 Z M10.5 8.25C10.5 7.2835 11.2835 6.5 12.25 6.5H24V15.25C24 17.3211 25.6789 19 27.75 19H37.5V39.75C37.5 40.7165 36.7165 41.5 35.75 41.5H24.2608C23.7353 42.4086 23.1029 43.2476 22.3809 44H35.75C38.0972 44 40 42.0972 40 39.75V18.4142C40 17.8175 39.7629 17.2452 39.341 16.8232L27.1768 4.65901C26.7548 4.23705 26.1825 4 25.5858 4H12.25C9.90279 4 8 5.90279 8 8.25V22.9963C8.79632 22.6642 9.63275 22.4091 10.5 22.2402V8.25ZM35.4822 16.5H27.75C27.0596 16.5 26.5 15.9404 26.5 15.25V7.51777L35.4822 16.5Z M24 35C24 41.0751 19.0751 46 13 46C6.92487 46 2 41.0751 2 35C2 28.9249 6.92487 24 13 24C19.0751 24 24 28.9249 24 35ZM11.7071 29.2929C11.3166 28.9024 10.6834 28.9024 10.2929 29.2929L5.29289 34.2929C4.90237 34.6834 4.90237 35.3166 5.29289 35.7071L10.2929 40.7071C10.6834 41.0976 11.3166 41.0976 11.7071 40.7071C12.0976 40.3166 12.0976 39.6834 11.7071 39.2929L8.41421 36H20C20.5523 36 21 35.5523 21 35C21 34.4477 20.5523 34 20 34H8.41421L11.7071 30.7071C12.0976 30.3166 12.0976 29.6834 11.7071 29.2929Z M31.8839 8.36612C32.372 8.85427 32.372 9.64573 31.8839 10.1339L18.0178 24L31.8839 37.8661C32.372 38.3543 32.372 39.1457 31.8839 39.6339C31.3957 40.122 30.6043 40.122 30.1161 39.6339L15.3661 24.8839C14.878 24.3957 14.878 23.6043 15.3661 23.1161L30.1161 8.36612C30.6043 7.87796 31.3957 7.87796 31.8839 8.36612Z + M17.25,19 C17.6642136,19 18,19.3357864 18,19.75 C18,20.1642136 17.6642136,20.5 17.25,20.5 L10.75,20.5 C10.3357864,20.5 10,20.1642136 10,19.75 C10,19.3357864 10.3357864,19 10.75,19 L17.25,19 Z M21.25,13 C21.6642136,13 22,13.3357864 22,13.75 C22,14.1642136 21.6642136,14.5 21.25,14.5 L6.75,14.5 C6.33578644,14.5 6,14.1642136 6,13.75 C6,13.3357864 6.33578644,13 6.75,13 L21.25,13 Z M24.25,7 C24.6642136,7 25,7.33578644 25,7.75 C25,8.16421356 24.6642136,8.5 24.25,8.5 L3.75,8.5 C3.33578644,8.5 3,8.16421356 3,7.75 C3,7.33578644 3.33578644,7 3.75,7 L24.25,7 Z @@ -317,4 +318,8 @@ + + diff --git a/Knossos.NET/Classes/Knossos.cs b/Knossos.NET/Classes/Knossos.cs index d0a2f268..738f2c83 100644 --- a/Knossos.NET/Classes/Knossos.cs +++ b/Knossos.NET/Classes/Knossos.cs @@ -702,12 +702,18 @@ public async static void LoadBasePath(bool isQuickLaunch = false) { if (globalSettings.basePath != null) { + //Clear Mod Tags + ModTags.ClearTags(); + MainWindowViewModel.Instance?.tagFilter.Clear(); + //Load Hardcoded tags + ModTags.AddHardcodedModTags(); + await FolderSearchRecursive(globalSettings.basePath, isQuickLaunch).ConfigureAwait(false); if (!isQuickLaunch) { //Sort/Re-sort installed mods - MainWindowViewModel.Instance?.InstalledModsView?.ChangeSort(MainWindowViewModel.Instance?.sharedSortType!); + MainWindowViewModel.Instance?.InstalledModsView?.ChangeSort(globalSettings.sortType); //Red border for mod with missing deps Dispatcher.UIThread.Invoke(() => @@ -733,6 +739,13 @@ await Task.Run(async () => { { await QuickLaunch(); } + + //generate mod tag buttons + Dispatcher.UIThread.Invoke(() => + { + NebulaModListView.Instance?.GenerateFilterButtons(); + ModListView.Instance?.GenerateFilterButtons(); + }); } } diff --git a/Knossos.NET/Classes/ModTags.cs b/Knossos.NET/Classes/ModTags.cs new file mode 100644 index 00000000..672144dd --- /dev/null +++ b/Knossos.NET/Classes/ModTags.cs @@ -0,0 +1,251 @@ +using HarfBuzzSharp; +using Knossos.NET.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Knossos.NET.Classes +{ + public class ModTag + { + public ModTag() + { + + } + + public ModTag(string modID) + { + ModID = modID; + } + + public ModTag(string modID, string tag) + { + ModID = modID; + Tags.Add(tag.ToLower()); + } + + public ModTag(string modID, List tags) + { + ModID = modID; + Tags = tags; + } + + public void AddTag(string tag) + { + Tags.Add(tag.ToLower()); + } + + public bool TagExist(string tag) + { + return Tags.Contains(tag.ToLower()); + } + + public List GetTags() + { + return Tags; + } + + public string ModID { get; private set; } = string.Empty; + private List Tags { get; set; } = new List(); + } + + /// + /// Handlers for the mod tag system + /// Each mod id can contain any number of tags + /// Tags are loaded from nebula and local locals at start + /// Then we can check here if a mod ID contains a tag + /// Note: + /// tags are case insensitive, but ModIDs arent! + /// + public static class ModTags + { + private static List modTags = new List(); + + /// + /// Use _ for spaces and remember these are checked with string.compare so no tags that can contain another + /// + public enum Tags + { + Total_Conversion, + Retail_FS2, + FS2_MOD, + FS1_MOD, + TC_MOD, + Utility, + Dependency, + VR_MOD, + MediaVPs, + Demo, + Multiplayer, + Testing + } + + public static void ClearTags() + { + modTags.Clear(); + } + + /// + /// Get a list of all tags loaded, without the mod id + /// + /// List + public static List GetListAllTags() + { + List list = new List(); + foreach (var modtag in modTags) + { + foreach (var tags in modtag.GetTags()) + { + if (!list.Contains(tags)) + list.Add(tags); + } + } + return list; + } + + /// + /// Checks if a mod id contains a tag + /// + /// + /// + /// true/false + public static bool IsTagPresentInModID(string modid, string tag) + { + var idTags = modTags.FirstOrDefault(x => x.ModID == modid); + if (idTags != null) + { + return idTags.TagExist(tag); + } + return false; + } + + /// + /// Adds a mod tag to the list + /// If the id and tag dosent exist already + /// + /// + /// + public static void AddModTag(string id, string tag) + { + var idFound = modTags.FirstOrDefault(x => x.ModID == id); + if (idFound != null) + { + if (!idFound.TagExist(tag)) + idFound.AddTag(tag); + } + else + { + modTags.Add(new ModTag(id, tag)); + } + } + + /// + /// Generate runtime mod tags for this mod + /// Tags will be determined based on avalible mod information + /// + /// + public static void AddModTagsRuntime(Mod? mod) + { + if(mod == null || mod.id.ToLower() == "fs2") return; + if(mod.parent == null) + { + AddModTag(mod.id,Tags.Total_Conversion.ToString()); + } + else + { + if (mod.parent.ToLower() == "fs2") + { + if(mod.GetDependency("fsport", true) != null) + { + AddModTag(mod.id, Tags.FS1_MOD.ToString()); // only going to work for installed mods + } + else + { + AddModTag(mod.id, Tags.FS2_MOD.ToString()); + } + } + else + { + AddModTag(mod.id, Tags.TC_MOD.ToString()); + } + } + } + + /// + /// List of harcoded tags, this is temporal until tags are implemented in nebula + /// + public static void AddHardcodedModTags() + { + AddModTag("FS2", Tags.Retail_FS2.ToString()); + + AddModTag("fsport", Tags.FS1_MOD.ToString()); + AddModTag("fsport-mediavps", Tags.FS1_MOD.ToString()); + AddModTag("fsport-mediavps", Tags.MediaVPs.ToString()); + AddModTag("str", Tags.FS1_MOD.ToString()); + AddModTag("denebiii", Tags.FS1_MOD.ToString()); + AddModTag("awakenings", Tags.FS1_MOD.ToString()); + AddModTag("the_aftermath_ribos", Tags.FS1_MOD.ToString()); + AddModTag("RetreatfromDenebCinematic", Tags.FS1_MOD.ToString()); + AddModTag("tombaugh_attack_cinematic", Tags.FS1_MOD.ToString()); + + AddModTag("MjnMHs", Tags.Dependency.ToString()); + AddModTag("SCPUI", Tags.Dependency.ToString()); + + AddModTag("Wing_Commander_Saga", Tags.Total_Conversion.ToString()); + + AddModTag("MVPS", Tags.MediaVPs.ToString()); + AddModTag("MVPS", Tags.FS2_MOD.ToString()); + + AddModTag("BWO_Demo", Tags.FS2_MOD.ToString()); + AddModTag("BWO_Demo", Tags.Demo.ToString()); + + AddModTag("fs2_demo", Tags.FS2_MOD.ToString()); + AddModTag("fs2_demo", Tags.Demo.ToString()); + AddModTag("fs2_org_demo", Tags.Demo.ToString()); + AddModTag("fs2_org_demo", Tags.Total_Conversion.ToString()); + + AddModTag("WCIV_Demo", Tags.Demo.ToString()); + AddModTag("WCIV_Demo", Tags.TC_MOD.ToString()); + + AddModTag("Solaris", Tags.Total_Conversion.ToString()); + AddModTag("wod", Tags.Total_Conversion.ToString()); + AddModTag("Diaspora_Release_Version", Tags.Total_Conversion.ToString()); + + AddModTag("rogue", Tags.FS2_MOD.ToString()); + AddModTag("blueplanetcomplete", Tags.FS2_MOD.ToString()); + + AddModTag("fs1coopup", Tags.FS1_MOD.ToString()); + AddModTag("fs1coopup", Tags.Multiplayer.ToString()); + AddModTag("fs2coopup", Tags.FS2_MOD.ToString()); + AddModTag("fs2coopup", Tags.Multiplayer.ToString()); + AddModTag("strcoopup", Tags.FS1_MOD.ToString()); + AddModTag("strcoopup", Tags.Multiplayer.ToString()); + + AddModTag("frontlines", Tags.FS1_MOD.ToString()); + AddModTag("jad", Tags.FS2_MOD.ToString()); + + AddModTag("BTA_Standalone", Tags.FS2_MOD.ToString()); + AddModTag("BtA", Tags.FS2_MOD.ToString()); + AddModTag("BtA", Tags.TC_MOD.ToString()); + + AddModTag("vr_mvps", Tags.VR_MOD.ToString()); + AddModTag("vr_mvps_fsport", Tags.VR_MOD.ToString()); + AddModTag("VRGC", Tags.VR_MOD.ToString()); + + AddModTag("CP_m", Tags.Utility.ToString()); + AddModTag("CP_M_FS1", Tags.Utility.ToString()); + + AddModTag("mlteset", Tags.Testing.ToString()); + AddModTag("ParticlesStressTesting", Tags.Testing.ToString()); + AddModTag("STIG", Tags.Testing.ToString()); + AddModTag("qaz_1", Tags.Testing.ToString()); + AddModTag("itsatestnumbnuts", Tags.Testing.ToString()); + AddModTag("FSPcustom", Tags.Testing.ToString()); + AddModTag("Stress_Test_Multi_With_Silly_mission", Tags.Testing.ToString()); + AddModTag("UITest", Tags.Testing.ToString()); + } + + } +} diff --git a/Knossos.NET/Knossos.NET.csproj b/Knossos.NET/Knossos.NET.csproj index 48c302fb..b11acfef 100644 --- a/Knossos.NET/Knossos.NET.csproj +++ b/Knossos.NET/Knossos.NET.csproj @@ -83,6 +83,7 @@ + diff --git a/Knossos.NET/Models/GlobalSettings.cs b/Knossos.NET/Models/GlobalSettings.cs index 29dc644c..5f87c691 100644 --- a/Knossos.NET/Models/GlobalSettings.cs +++ b/Knossos.NET/Models/GlobalSettings.cs @@ -165,9 +165,9 @@ public bool hideBuildNightly } [JsonIgnore] - private MainWindowViewModel.SortType _sortType = MainWindowViewModel.SortType.name; + private ModSortType _sortType = ModSortType.name; [JsonPropertyName("last_sort_type"), JsonConverter(typeof(JsonStringEnumConverter))] - public MainWindowViewModel.SortType sortType + public ModSortType sortType { get { return _sortType; } set { if (_sortType != value) { _sortType = value; pendingChangesOnAppClose = true; } } @@ -915,7 +915,7 @@ public void Save(bool writeIni = true) { // Quickly update the sort type which is managed elsewhere if (MainWindowViewModel.Instance != null){ - sortType = MainWindowViewModel.Instance.sharedSortType; + sortType = Knossos.globalSettings.sortType; } var options = new JsonSerializerOptions diff --git a/Knossos.NET/Models/Mod.cs b/Knossos.NET/Models/Mod.cs index d8f6da6c..e03cac6a 100644 --- a/Knossos.NET/Models/Mod.cs +++ b/Knossos.NET/Models/Mod.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using IniParser; using System.Linq; +using Knossos.NET.ViewModels; +using System.Globalization; namespace Knossos.NET.Models { @@ -844,6 +846,71 @@ public static int CompareTitles(string title1, string title2) return String.Compare(KnUtils.RemoveArticles(title1), KnUtils.RemoveArticles(title2), StringComparison.CurrentCultureIgnoreCase); } + /// + /// Does mod sorting based on the global Knossos.globalSettings.sortType variable + /// Returns: + /// Less than zero if modA precedes modB + /// Zero if they are equal + /// Higher than zero if modA follows mobB + /// + /// + /// + /// + public static int SortMods(Mod? modA, Mod? modB) + { + if (modA == null && modB == null) return 0; + if (modA == null && modB != null) return 1; + if (modA != null && modB == null) return -1; + try + { + switch (Knossos.globalSettings.sortType) + { + case ModSortType.name: + return Mod.CompareTitles(modA!.title, modB!.title); + case ModSortType.release: + if (modA!.firstRelease == modB!.firstRelease) + return 0; + if (modA.firstRelease != null && modB.firstRelease != null) + { + if (DateTime.Parse(modA.firstRelease, CultureInfo.InvariantCulture) < DateTime.Parse(modB.firstRelease, CultureInfo.InvariantCulture)) + return 1; + else + return -1; + } + else + { + if (modA.firstRelease == null) + return -1; + else + return 1; + } + case ModSortType.update: + if (modA!.lastUpdate == modB!.lastUpdate) + return 0; + if (modA.lastUpdate != null && modB.lastUpdate != null) + { + if (DateTime.Parse(modA.lastUpdate, CultureInfo.InvariantCulture) < DateTime.Parse(modB.lastUpdate, CultureInfo.InvariantCulture)) + return 1; + else + return -1; + } + else + { + if (modA.lastUpdate == null) + return 1; + else + return -1; + } + default: return 0; + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Warning, "NebulaModCardViewModel.CompareMods()", ex.Message); + return 0; + } + } + /// /// 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/ModListViewModel.cs b/Knossos.NET/ViewModels/ModListViewModel.cs index b59d7504..308eceb0 100644 --- a/Knossos.NET/ViewModels/ModListViewModel.cs +++ b/Knossos.NET/ViewModels/ModListViewModel.cs @@ -1,11 +1,10 @@ using System; -using System.Collections.ObjectModel; -using System.Globalization; -using System.IO; using System.Linq; -using Avalonia.Threading; +using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; +using Knossos.NET.Classes; using Knossos.NET.Models; +using ObservableCollections; namespace Knossos.NET.ViewModels { @@ -14,11 +13,8 @@ namespace Knossos.NET.ViewModels /// public partial class ModListViewModel : ViewModelBase { - /// - /// Is this tab in the middle of sorting or filtering mod tiles - /// [ObservableProperty] - internal bool sorting = false; + internal bool sorting = true; /// /// For the knossos Loading animation @@ -29,47 +25,112 @@ public partial class ModListViewModel : ViewModelBase /// /// Current Sort Mode /// - internal MainWindowViewModel.SortType sortType = MainWindowViewModel.SortType.unsorted; + internal ModSortType localSort = ModSortType.name; internal string search = string.Empty; internal string Search { get { return search; } - set + set { - if (value != Search){ + if (value != Search) + { this.SetProperty(ref search, value); - if (value.Trim() != string.Empty) - { - foreach(var mod in Mods) - { - if( mod.Name != null && mod.Name.ToLower().Contains(value.ToLower())) - { - mod.Visible = true; - } - else - { - mod.Visible = false; - } - } - } - else - { - Mods.ForEach(m => m.Visible = true); - } + ApplyFilters(); } } } - [ObservableProperty] - internal ObservableCollection mods = new ObservableCollection(); + //The actual collection were the mods are + private ObservableList Mods = new ObservableList(); + //A hook for the UI, do not access directly + internal NotifyCollectionChangedSynchronizedViewList CardsView { get; set; } public ModListViewModel() { LoadingAnimation = new LoadingIconViewModel(); + CardsView = Mods.ToNotifyCollectionChangedSlim(SynchronizationContextCollectionEventDispatcher.Current); + } + + public void ApplyTagFilter(int tagIndex) + { + if (MainWindowViewModel.Instance != null) + { + var tags = ModTags.GetListAllTags(); + if (tags.Count() > tagIndex) + { + MainWindowViewModel.Instance.tagFilter.Add(tags[tagIndex]); + MainWindowViewModel.Instance.tagFilterChanged = true; + } + ApplyFilters(); + } + } + + public void RemoveTagFilter(int tagIndex) + { + if (MainWindowViewModel.Instance != null) + { + var tags = ModTags.GetListAllTags(); + if (tags.Count() > tagIndex) + { + MainWindowViewModel.Instance.tagFilter.Remove(tags[tagIndex]); + MainWindowViewModel.Instance.tagFilterChanged = true; + } + ApplyFilters(); + } + } + + private void ApplyFilters() + { + Parallel.ForEach(Mods, new ParallelOptions { MaxDegreeOfParallelism = 4 }, card => + { + bool visibility = true; + //By search + if (Search.Trim() != string.Empty) + { + if (card.Name == null || !card.Name.Contains(Search, StringComparison.CurrentCultureIgnoreCase)) + { + visibility = false; + } + } + //Tags + if (visibility && MainWindowViewModel.Instance != null && MainWindowViewModel.Instance.tagFilter.Any()) + { + visibility = false; + foreach (var tag in MainWindowViewModel.Instance.tagFilter) + { + if (card.ID != null && ModTags.IsTagPresentInModID(card.ID, tag)) + { + visibility = true; + break; + } + } + } + card.Visible = visibility; + }); + } + /// + /// + /// + public void OpenTab() + { + if (MainWindowViewModel.Instance != null) + { + Search = MainWindowViewModel.Instance.sharedSearch; + if (MainWindowViewModel.Instance.tagFilterChanged) + { + ApplyFilters(); + MainWindowViewModel.Instance.tagFilterChanged = false; + } + } } + public void CloseTab() + { + if (MainWindowViewModel.Instance != null) + MainWindowViewModel.Instance.sharedSearch = Search; + } /// /// Clears all mods in view @@ -102,13 +163,14 @@ public void AddMod(Mod modJson) { if (Mods[i].ActiveVersion != null) { - if(CompareMods(Mods[i].ActiveVersion!,modJson) > 0) + if(Mod.SortMods(Mods[i].ActiveVersion!,modJson) > 0) { break; } } } Mods.Insert(i, new ModCardViewModel(modJson)); + ModTags.AddModTagsRuntime(modJson); } else { @@ -122,107 +184,25 @@ public void AddMod(Mod modJson) /// internal void ChangeSort(object sort) { - try + Sorting = true; + LoadingAnimation.Animate = 1; + var newSort = ModSortType.unsorted; + if (sort is ModSortType) { - MainWindowViewModel.SortType newSort; - - if (sort is MainWindowViewModel.SortType){ - newSort = (MainWindowViewModel.SortType)sort; - } else { - newSort = (MainWindowViewModel.SortType)Enum.Parse(typeof(MainWindowViewModel.SortType), (string)sort); - } - - if (newSort != sortType) - { - if (MainWindowViewModel.Instance != null && newSort != MainWindowViewModel.SortType.unsorted && MainWindowViewModel.Instance.sharedSortType != newSort ) - { - MainWindowViewModel.Instance.sharedSortType = newSort; - } - LoadingAnimation.Animate = 1; - Sorting = true; - Dispatcher.UIThread.Invoke( () => - { - var tempList = Mods.ToList(); - // Only sort and update to the new sort type if we have mods to sort! - if (tempList.Any()){ - sortType = newSort; - tempList.Sort(CompareMods); - for (int i = 0; i < tempList.Count; i++) - { - Mods.Move(Mods.IndexOf(tempList[i]), i); - } - } - GC.Collect(); - Sorting = false; - LoadingAnimation.Animate = 0; - },DispatcherPriority.Background); - - } - }catch(Exception ex) - { - Sorting = false; - LoadingAnimation.Animate = 0; - Log.Add(Log.LogSeverity.Error, "ModListViewModel.ChangeSort()", ex); + newSort = (ModSortType)sort; } - } - - private int CompareMods(ModCardViewModel x, ModCardViewModel y) - { - if (x.ActiveVersion != null && y.ActiveVersion != null) - return CompareMods(x.ActiveVersion, y.ActiveVersion); else - return 0; - } - - private int CompareMods(Mod modA,Mod modB) - { - try { - switch (sortType) - { - case MainWindowViewModel.SortType.name: - return Mod.CompareTitles(modA.title, modB.title); - case MainWindowViewModel.SortType.release: - if (modA.firstRelease == modB.firstRelease) - return 0; - if (modA.firstRelease != null && modB.firstRelease != null) - { - if (DateTime.Parse(modA.firstRelease, CultureInfo.InvariantCulture) < DateTime.Parse(modB.firstRelease, CultureInfo.InvariantCulture)) - return 1; - else - return -1; - } - else - { - if (modA.firstRelease == null) - return -1; - else - return 1; - } - case MainWindowViewModel.SortType.update: - if (modA.lastUpdate == modB.lastUpdate) - return 0; - if (modA.lastUpdate != null && modB.lastUpdate != null) - { - if (DateTime.Parse(modA.lastUpdate, CultureInfo.InvariantCulture) < DateTime.Parse(modB.lastUpdate, CultureInfo.InvariantCulture)) - return 1; - else - return -1; - } - else - { - if (modA.lastUpdate == null) - return 1; - else - return -1; - } - default: return 0; - } - }catch(Exception ex) - { - Log.Add(Log.LogSeverity.Warning, "ModListViewModel.CompareMods()",ex.Message); - return 0; + newSort = (ModSortType)Enum.Parse(typeof(ModSortType), (string)sort); + } + if (newSort != localSort) + { + localSort = newSort; + Knossos.globalSettings.sortType = newSort; + Mods.Sort(); //It will use ModCardViewModel.CompareTo() } + LoadingAnimation.Animate = 0; + Sorting = false; } /// diff --git a/Knossos.NET/ViewModels/NebulaModListViewModel.cs b/Knossos.NET/ViewModels/NebulaModListViewModel.cs index 74a82c0b..de74a099 100644 --- a/Knossos.NET/ViewModels/NebulaModListViewModel.cs +++ b/Knossos.NET/ViewModels/NebulaModListViewModel.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; +using Knossos.NET.Classes; using Knossos.NET.Models; +using ObservableCollections; namespace Knossos.NET.ViewModels { @@ -16,12 +15,6 @@ namespace Knossos.NET.ViewModels /// public partial class NebulaModListViewModel : ViewModelBase { - - /// - /// Current Sort Mode - /// - internal MainWindowViewModel.SortType sortType = MainWindowViewModel.SortType.unsorted; - /// /// The user has opened this tab in this session? /// @@ -32,7 +25,7 @@ internal bool isLoading{ get { return _isLoading; } set { _isLoading = value; - ShowTiles = !sorting && !isLoading; + ShowTiles = !isLoading; if (ShowTiles) { LoadingAnimation.Animate = 0; @@ -44,29 +37,11 @@ internal bool isLoading{ } } - private bool _sorting = true; - internal bool sorting { - get { return _sorting; } - set { - _sorting = value; - ShowTiles = !sorting && !isLoading; - - if (ShowTiles) - { - LoadingAnimation.Animate = 0; - } - else - { - LoadingAnimation.Animate = 1; - } - } - } - /// - /// For the UI to detmerine whether to show mod tiles. It needs to check more than one property, so this gets updated when sorting or isLoading do. + /// For the UI to detmerine whether to show mod tiles. It needs to check more than one property /// [ObservableProperty] - internal bool showTiles = false; + public bool showTiles = false; [ObservableProperty] internal LoadingIconViewModel loadingAnimation = new LoadingIconViewModel(); @@ -80,91 +55,123 @@ internal string Search get { return search; } set { - sorting = true; - LoadingAnimation.Animate = 1; - - if (value != Search){ + if (value != Search) + { this.SetProperty(ref search, value); - if (value.Trim() != string.Empty) - { - foreach(var mod in Mods) - { - if( mod.Name != null && mod.Name.ToLower().Contains(value.ToLower())) - { - mod.Visible = true; - } - else - { - mod.Visible = false; - } - } - } - else - { - Mods.ForEach(m => m.Visible = true); - } + ApplyFilters(); } - sorting = false; - LoadingAnimation.Animate = 1; } } - [ObservableProperty] - internal ObservableCollection mods = new ObservableCollection(); + private ModSortType localSort = ModSortType.name; + //The actual collection were the mods are + private ObservableList Mods = new ObservableList(); + //A hook for the UI, do not access directly + internal NotifyCollectionChangedSynchronizedViewList CardsView { get; set; } public NebulaModListViewModel() { + CardsView = Mods.ToNotifyCollectionChangedSlim(SynchronizationContextCollectionEventDispatcher.Current); } - /// - /// Open the tab and slowly display modcards to avoid ui lock - /// - public async void OpenTab(string newSearch, MainWindowViewModel.SortType newSortType) + public void ApplyTagFilter(int tagIndex) { - Search = newSearch; - if (isLoading) + if (MainWindowViewModel.Instance != null) { - IsTabOpen = true; - return; + var tags = ModTags.GetListAllTags(); + if(tags.Count() > tagIndex) + { + MainWindowViewModel.Instance.tagFilter.Add(tags[tagIndex]); + MainWindowViewModel.Instance.tagFilterChanged = true; + } + ApplyFilters(); } + } - if (!IsTabOpen) + public void RemoveTagFilter(int tagIndex) + { + if (MainWindowViewModel.Instance != null) { - IsTabOpen = true; - // This should remain true until we get to Change Sort. It is guaranteed to be finished then - sorting = true; + var tags = ModTags.GetListAllTags(); + if (tags.Count() > tagIndex) + { + MainWindowViewModel.Instance.tagFilter.Remove(tags[tagIndex]); + MainWindowViewModel.Instance.tagFilterChanged = true; + } + ApplyFilters(); + } + } - try + private void ApplyFilters() + { + Parallel.ForEach(Mods, new ParallelOptions { MaxDegreeOfParallelism = 4 }, card => + { + bool visibility = true; + //By search + if (Search.Trim() != string.Empty) { - await Task.Delay(200).ConfigureAwait(false); - List? modsInView = null; - await Dispatcher.UIThread.InvokeAsync(() => + if (card.Name == null || !card.Name.Contains(Search, StringComparison.CurrentCultureIgnoreCase)) { - isLoading = false; - modsInView = Mods.ToList(); - }); - if (modsInView != null) + visibility = false; + } + } + //Tags + if (visibility && MainWindowViewModel.Instance != null && MainWindowViewModel.Instance.tagFilter.Any()) + { + visibility = false; + foreach (var tag in MainWindowViewModel.Instance.tagFilter) { - foreach (var m in modsInView) + if(card.ID != null && ModTags.IsTagPresentInModID(card.ID,tag)) { - await Dispatcher.UIThread.InvokeAsync(() => - { - if (Search.Trim() == string.Empty || m.Name != null && m.Name.ToLower().Contains(Search.ToLower())) - { - m.Visible = true; - } - }); - await Task.Delay(1).ConfigureAwait(false); + visibility = true; + break; } } } - catch (Exception ex) + card.Visible = visibility; + }); + } + + /// + /// Open the tab code + /// + public void OpenTab() + { + IsTabOpen = true; + if (!isLoading) + { + Task.Run(() => { - Log.Add(Log.LogSeverity.Error, "NebulaModListViewModel.OpenTab", ex); - } + ShowTiles = false; + if (MainWindowViewModel.Instance != null) + { + Search = MainWindowViewModel.Instance.sharedSearch; + if (MainWindowViewModel.Instance.tagFilterChanged) + { + ApplyFilters(); + MainWindowViewModel.Instance.tagFilterChanged = false; + } + } + ChangeSort(Knossos.globalSettings.sortType); + Parallel.ForEach(Mods, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async card => + { + await card.LoadImage(); + }); + ShowTiles = true; + }); + } + } + /// + /// Close the tab code + /// + public void CloseTab() + { + ShowTiles = false; + if (MainWindowViewModel.Instance != null) + { + MainWindowViewModel.Instance.sharedSearch = Search; } - ChangeSort(newSortType); } /// @@ -192,26 +199,12 @@ public void AddMod(Mod modJson) var modCard = Mods.FirstOrDefault(m => m.ID == modJson.id); if (modCard == null) { - int i; - for (i = 0; i < Mods.Count; i++) - { - if (Mods[i].modJson != null) - { - if(CompareMods(Mods[i].modJson!,modJson) > 0) - { - break; - } - } - } var card = new NebulaModCardViewModel(modJson); - if (!isLoading) - { - if (Search.Trim() == string.Empty || card.Name != null && card.Name.ToLower().Contains(Search.ToLower())) - { - card.Visible = true; - } - } - Mods.Insert(i, card); + ModTags.AddModTagsRuntime(modJson); + Mods.Add(card); + Mods.Sort(); + if(IsTabOpen) + _ = card.LoadImage(); } else { @@ -220,48 +213,29 @@ public void AddMod(Mod modJson) } /// - /// Adds a new list of mods into the, in a more efficient way that loading one by one - /// It replaces all mods, all old mods are deleted, intended only for the first big load of mods + /// Adds a new list of mods into the view /// /// - public async void AddMods(List modList) + public void AddMods(List modList) { - isLoading = true; - await Task.Delay(20).ConfigureAwait(false); - var newModCardList = new ObservableCollection(); - foreach (Mod? mod in modList) - { - var modCard = newModCardList.FirstOrDefault(m => m.ID == mod.id); - if (modCard == null) + Task.Factory.StartNew(() => { + Parallel.ForEach(modList, new ParallelOptions { MaxDegreeOfParallelism = 4 }, mod => + { + Mods.Add(new NebulaModCardViewModel(mod)); + }); + Mods.Sort(); + if(IsTabOpen) { - int i; - for (i = 0; i < newModCardList.Count; i++) + Parallel.ForEach(Mods, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async card => { - if (newModCardList[i].modJson != null) - { - if (CompareMods(newModCardList[i].modJson!, mod) > 0) - { - break; - } - } - } - var card = new NebulaModCardViewModel(mod); - newModCardList.Insert(i, card); + await card.LoadImage(); + }); } - else + foreach(var mod in modList) { - //Update? Should NOT be needed for Nebula mods + ModTags.AddModTagsRuntime(mod); } - } - await Dispatcher.UIThread.InvokeAsync(() => - { - Mods = newModCardList; isLoading = false; - if (IsTabOpen) - { - IsTabOpen = false; - OpenTab(Search, sortType); - } }); } @@ -271,107 +245,20 @@ await Dispatcher.UIThread.InvokeAsync(() => /// internal void ChangeSort(object sort) { - try - { - MainWindowViewModel.SortType newSort; - - if (sort is MainWindowViewModel.SortType){ - newSort = (MainWindowViewModel.SortType)sort; - } else { - newSort = (MainWindowViewModel.SortType)Enum.Parse(typeof(MainWindowViewModel.SortType), (string)sort); - } - - if (newSort != sortType) - { - if (MainWindowViewModel.Instance != null && newSort != MainWindowViewModel.SortType.unsorted && MainWindowViewModel.Instance.sharedSortType != newSort) - { - MainWindowViewModel.Instance.sharedSortType = newSort; - } - if (sortType != MainWindowViewModel.SortType.unsorted) - { - sorting = true; - } - - Dispatcher.UIThread.Invoke( () => - { - sortType = newSort; - var tempList = Mods.ToList(); - tempList.Sort(CompareMods); - isLoading = true; - for (int i = 0; i < tempList.Count; i++) - { - Mods.Move(Mods.IndexOf(tempList[i]), i); - } - isLoading = false; - GC.Collect(); - },DispatcherPriority.Background); - } - }catch(Exception ex) + var newSort = ModSortType.unsorted; + if (sort is ModSortType) { - Log.Add(Log.LogSeverity.Error, "ModListViewModel.ChangeSort()", ex); + newSort = (ModSortType)sort; } - - // There is no reason to keep this on, whether in success or fail, and some of the functions that call this - // set sorting to true. - sorting = false; - } - - private int CompareMods(NebulaModCardViewModel x, NebulaModCardViewModel y) - { - if (x.modJson != null && y.modJson != null) - return CompareMods(x.modJson, y.modJson); else - return 0; - } - - private int CompareMods(Mod modA,Mod modB) - { - try { - switch (sortType) - { - case MainWindowViewModel.SortType.name: - return Mod.CompareTitles(modA.title, modB.title); - case MainWindowViewModel.SortType.release: - if (modA.firstRelease == modB.firstRelease) - return 0; - if (modA.firstRelease != null && modB.firstRelease != null) - { - if (DateTime.Parse(modA.firstRelease, CultureInfo.InvariantCulture) < DateTime.Parse(modB.firstRelease, CultureInfo.InvariantCulture)) - return 1; - else - return -1; - } - else - { - if (modA.firstRelease == null) - return -1; - else - return 1; - } - case MainWindowViewModel.SortType.update: - if (modA.lastUpdate == modB.lastUpdate) - return 0; - if (modA.lastUpdate != null && modB.lastUpdate != null) - { - if (DateTime.Parse(modA.lastUpdate, CultureInfo.InvariantCulture) < DateTime.Parse(modB.lastUpdate, CultureInfo.InvariantCulture)) - return 1; - else - return -1; - } - else - { - if (modA.lastUpdate == null) - return 1; - else - return -1; - } - default: return 0; - } - }catch(Exception ex) - { - Log.Add(Log.LogSeverity.Warning, "NebulaModListViewModel.CompareMods()",ex.Message); - return 0; + newSort = (ModSortType)Enum.Parse(typeof(ModSortType), (string)sort); + } + if (newSort != localSort) + { + localSort = newSort; + Knossos.globalSettings.sortType = newSort; + Mods.Sort(); //It will use NebulaModCardViewModel.CompareTo() } } diff --git a/Knossos.NET/ViewModels/Templates/ModCardViewModel.cs b/Knossos.NET/ViewModels/Templates/ModCardViewModel.cs index 06e56752..dac64f27 100644 --- a/Knossos.NET/ViewModels/Templates/ModCardViewModel.cs +++ b/Knossos.NET/ViewModels/Templates/ModCardViewModel.cs @@ -15,7 +15,7 @@ namespace Knossos.NET.ViewModels { - public partial class ModCardViewModel : ViewModelBase + public partial class ModCardViewModel : ViewModelBase, IComparable { /* General Mod variables */ private ModDetailsView? detailsView = null; @@ -360,5 +360,12 @@ private void LoadImage() Log.Add(Log.LogSeverity.Warning, "ModCardViewModel.LoadImage", ex); } } + + public int CompareTo(ModCardViewModel? other) + { + if (other == null) + return -1; + return Mod.SortMods(ActiveVersion, other.ActiveVersion); + } } } diff --git a/Knossos.NET/ViewModels/Templates/NebulaModCardViewModel.cs b/Knossos.NET/ViewModels/Templates/NebulaModCardViewModel.cs index 3dee01d9..bdecbf12 100644 --- a/Knossos.NET/ViewModels/Templates/NebulaModCardViewModel.cs +++ b/Knossos.NET/ViewModels/Templates/NebulaModCardViewModel.cs @@ -5,14 +5,13 @@ using CommunityToolkit.Mvvm.ComponentModel; using Knossos.NET.Views; using System.IO; -using System.Net.Http; using System.Threading.Tasks; using System.Threading; using Avalonia.Threading; namespace Knossos.NET.ViewModels { - public partial class NebulaModCardViewModel : ViewModelBase + public partial class NebulaModCardViewModel : ViewModelBase, IComparable { private CancellationTokenSource? cancellationTokenSource = null; public Mod? modJson { get; set; } @@ -23,27 +22,8 @@ public partial class NebulaModCardViewModel : ViewModelBase internal string? ModVersion { get { return modJson != null ? modJson.version : null; } } [ObservableProperty] internal Bitmap? tileImage; - internal bool visible = false; - internal bool Visible - { - get - { - return visible; - } - set - { - if(visible != value) - { - SetProperty(ref visible, value); - if(value && TileImage == null && modJson != null) - { - Dispatcher.UIThread.Invoke(() => { - LoadImage(modJson.fullPath, modJson.tile); - }); - } - } - } - } + [ObservableProperty] + internal bool visible = true; [ObservableProperty] internal bool isInstalling = false; @@ -63,10 +43,21 @@ public NebulaModCardViewModel(Mod modJson) Log.Add(Log.LogSeverity.Information, "NebulaModCardViewModel(Constructor)", "Creating mod card for " + modJson); modJson.ClearUnusedData(); this.modJson = modJson; - //Moved to load when visible only + //Moved to load by external call only //LoadImage(modJson.fullPath, modJson.tile); } + /// + /// Calls to load the tile image + /// + public async Task LoadImage() + { + if (TileImage == null && modJson != null) + { + await LoadImage(modJson.fullPath, modJson.tile); + } + } + /* Button Commands */ internal void ButtonCommand(object command) { @@ -128,7 +119,7 @@ internal async void ButtonCommandDetails() } } - private void LoadImage(string modFullPath, string? tileString) + private async Task LoadImage(string modFullPath, string? tileString) { TileImage?.Dispose(); TileImage = new Bitmap(AssetLoader.Open(new Uri("avares://Knossos.NET/Assets/general/NebulaDefault.png"))); @@ -143,17 +134,11 @@ private void LoadImage(string modFullPath, string? tileString) } else { - Task.Run(async () => + using (var fs = await KnUtils.GetRemoteResourceStream(tileString).ConfigureAwait(false)) { - using (var fs = await KnUtils.GetRemoteResourceStream(tileString).ConfigureAwait(false)) - { - Dispatcher.UIThread.Invoke(() => - { - if (fs != null) - TileImage = new Bitmap(fs); - }); - } - }).ConfigureAwait(false); + if (fs != null) + TileImage = new Bitmap(fs); + } } } } @@ -162,5 +147,12 @@ private void LoadImage(string modFullPath, string? tileString) Log.Add(Log.LogSeverity.Warning, "NebulaModCardViewModel.LoadImage", ex); } } + + public int CompareTo(NebulaModCardViewModel? other) + { + if (other == null) + return -1; + return Mod.SortMods(modJson, other.modJson); + } } } diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 7781a0de..e805e1f7 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -14,6 +14,14 @@ namespace Knossos.NET.ViewModels { public record MainViewMenuItem(ViewModelBase vm, string? iconRoute, string label, string tooltip); + public enum ModSortType + { + name, + release, + update, + unsorted + } + /// /// Main Windows View Mode /// Everything starts here @@ -70,35 +78,11 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] internal int buttomListRow = 1; - - internal string sharedSearch = string.Empty; - + public string sharedSearch = string.Empty; public string LatestNightly = string.Empty; public string LatestStable = string.Empty; - - public enum SortType - { - name, - release, - update, - unsorted - } - - private SortType _sortType = SortType.name; //do not use directly - internal SortType sharedSortType - { - get { return _sortType; } - set - { - if (_sortType != value) - { - //change sort and update globalsettings value - //to be saved at app close - this.SetProperty(ref _sortType, value); - Knossos.globalSettings.sortType = value; - } - } - } + public List tagFilter { get; private set; } = new List(); + public bool tagFilterChanged = false; public MainWindowViewModel() { @@ -273,11 +257,11 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) // Things to do on tab exit if (InstalledModsView != null && CurrentViewModel == InstalledModsView) //Exiting the Play tab. { - sharedSearch = InstalledModsView.Search; + InstalledModsView.CloseTab(); } if (NebulaModsView != null && CurrentViewModel == NebulaModsView) //Exiting the Nebula tab. { - sharedSearch = NebulaModsView.Search; + NebulaModsView.CloseTab(); } if(GlobalSettingsView != null && CurrentViewModel == GlobalSettingsView) //Exiting the settings view { @@ -295,12 +279,11 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) //Run code when entering a new view if (CurrentViewModel == InstalledModsView) //Play Tab { - InstalledModsView.Search = sharedSearch; - InstalledModsView.ChangeSort(sharedSortType); + InstalledModsView.OpenTab(); } if (CurrentViewModel == NebulaModsView) //Nebula Mods { - NebulaModsView.OpenTab(sharedSearch, sharedSortType); + NebulaModsView.OpenTab(); } if (CurrentViewModel == DeveloperModView) //Dev Tab { @@ -488,10 +471,8 @@ internal void ApplySettings() { Dispatcher.UIThread.Invoke(() => { IsMenuOpen = Knossos.globalSettings.mainMenuOpen; - sharedSortType = Knossos.globalSettings.sortType; - InstalledModsView?.ChangeSort(sharedSortType); - if(NebulaModsView != null) - NebulaModsView.sortType = sharedSortType; + InstalledModsView?.ChangeSort(Knossos.globalSettings.sortType); + NebulaModsView?.ChangeSort(Knossos.globalSettings.sortType); }); } diff --git a/Knossos.NET/Views/ModListView.axaml b/Knossos.NET/Views/ModListView.axaml index fd2da1fd..0a6970e9 100644 --- a/Knossos.NET/Views/ModListView.axaml +++ b/Knossos.NET/Views/ModListView.axaml @@ -15,15 +15,34 @@ - - - - + + + + + + Sort by + + + + + + + + + + + Filters + + + + + + + + @@ -52,11 +71,11 @@ - - - + + + - + diff --git a/Knossos.NET/Views/ModListView.axaml.cs b/Knossos.NET/Views/ModListView.axaml.cs index 828b2f53..1adc518b 100644 --- a/Knossos.NET/Views/ModListView.axaml.cs +++ b/Knossos.NET/Views/ModListView.axaml.cs @@ -1,12 +1,180 @@ +using Avalonia; using Avalonia.Controls; +using Knossos.NET.Classes; +using Knossos.NET.ViewModels; +using System; +using System.Linq; +using System.Threading.Tasks; namespace Knossos.NET.Views { public partial class ModListView : UserControl { + public static ModListView? Instance; + private bool filterButtonsGenerated = false; + private StackPanel? sortPanel; + private WrapPanel? filterPanel; + public ModListView() { InitializeComponent(); + Instance = this; + AttachButtonUpdate(); + } + + private void AttachButtonUpdate() + { + try + { + sortPanel = this.FindControl("SortPanel"); + filterPanel = this.FindControl("FilterPanel"); + var filterFlyout = this.FindControl - - - + + + + + + Sort by + + + + + + + + + + + Filters + + + + + + + + @@ -37,14 +56,14 @@ - - - + + + - + - + diff --git a/Knossos.NET/Views/NebulaModListView.axaml.cs b/Knossos.NET/Views/NebulaModListView.axaml.cs index 6561b451..685e7ac6 100644 --- a/Knossos.NET/Views/NebulaModListView.axaml.cs +++ b/Knossos.NET/Views/NebulaModListView.axaml.cs @@ -1,12 +1,178 @@ +using Avalonia; using Avalonia.Controls; +using Knossos.NET.Classes; +using Knossos.NET.ViewModels; +using System; +using System.Linq; +using System.Threading.Tasks; namespace Knossos.NET.Views { public partial class NebulaModListView : UserControl { + public static NebulaModListView? Instance; + private bool filterButtonsGenerated = false; + private StackPanel? sortPanel; + private WrapPanel? filterPanel; + public NebulaModListView() { InitializeComponent(); + Instance = this; + AttachButtonUpdate(); + } + + private void AttachButtonUpdate() + { + try + { + sortPanel = this.FindControl("SortPanel"); + filterPanel = this.FindControl("FilterPanel"); + var filterFlyout = this.FindControl