From 558a011e00cb85b3b306a524e38f51d76e342625 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 15:02:13 +0000 Subject: [PATCH 1/8] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index d6ff8cf46..c4ea8bafd 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit d6ff8cf46c7e341989843c28c7550f8d50bee851 +Subproject commit c4ea8bafda8d88d49a390014dd3b86457f2dc7d5 From 689d2f01d9cd642bb77a249c22eac6c8d9884d67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 15:02:13 +0000 Subject: [PATCH 2/8] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 9c5f93cf3..2c3b35dd7 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 9c5f93cf3ac57236656cd2323b93cd258ea84a88 +Subproject commit 2c3b35dd7da71a94b517536affa180fd0e7dc403 From 62b8b0834cefa190e749a67a311ecc3e92dfc346 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 26 Dec 2025 16:09:32 +0100 Subject: [PATCH 3/8] Use v145 build tools for C++ components --- Dalamud.Boot/Dalamud.Boot.vcxproj | 4 ++-- DalamudCrashHandler/DalamudCrashHandler.vcxproj | 4 ++-- external/cimgui/cimgui.vcxproj | 6 +++--- external/cimguizmo/cimguizmo.vcxproj | 6 +++--- external/cimplot/cimplot.vcxproj | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index 0a4a9c563..20c107be2 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -25,7 +25,7 @@ DynamicLibrary true - v143 + v145 false Unicode bin\$(Configuration)\ @@ -211,4 +211,4 @@ - \ No newline at end of file + diff --git a/DalamudCrashHandler/DalamudCrashHandler.vcxproj b/DalamudCrashHandler/DalamudCrashHandler.vcxproj index 00d67cd65..e332b7b03 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.vcxproj +++ b/DalamudCrashHandler/DalamudCrashHandler.vcxproj @@ -25,7 +25,7 @@ Application true - v143 + v145 false Unicode ..\bin\$(Configuration)\ @@ -95,4 +95,4 @@ - \ No newline at end of file + diff --git a/external/cimgui/cimgui.vcxproj b/external/cimgui/cimgui.vcxproj index d99d23119..e047718ac 100644 --- a/external/cimgui/cimgui.vcxproj +++ b/external/cimgui/cimgui.vcxproj @@ -35,13 +35,13 @@ DynamicLibrary true - v143 + v145 Unicode DynamicLibrary false - v143 + v145 true Unicode @@ -106,4 +106,4 @@ - \ No newline at end of file + diff --git a/external/cimguizmo/cimguizmo.vcxproj b/external/cimguizmo/cimguizmo.vcxproj index 9bf692d4b..b3d66fb81 100644 --- a/external/cimguizmo/cimguizmo.vcxproj +++ b/external/cimguizmo/cimguizmo.vcxproj @@ -37,13 +37,13 @@ DynamicLibrary true - v143 + v145 Unicode DynamicLibrary false - v143 + v145 true Unicode @@ -108,4 +108,4 @@ - \ No newline at end of file + diff --git a/external/cimplot/cimplot.vcxproj b/external/cimplot/cimplot.vcxproj index e9aecbc15..cdc4226bf 100644 --- a/external/cimplot/cimplot.vcxproj +++ b/external/cimplot/cimplot.vcxproj @@ -35,13 +35,13 @@ DynamicLibrary true - v143 + v145 Unicode DynamicLibrary false - v143 + v145 true Unicode @@ -106,4 +106,4 @@ - \ No newline at end of file + From a659cd8a49df7c7370d7596e946202e9c5d1d3be Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 26 Dec 2025 16:15:51 +0100 Subject: [PATCH 4/8] Adjust to Excel renames --- .../Internal/Windows/Data/Widgets/UIColorWidget.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index 3550f053c..fd3f1d11c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -126,17 +126,17 @@ internal class UiColorWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_white"); - if (this.DrawColorColumn(row.Unknown0) && + if (this.DrawColorColumn(row.ClearWhite) && adjacentRow.HasValue) - DrawEdgePreview(id, row.Unknown0, adjacentRow.Value.Unknown0); + DrawEdgePreview(id, row.ClearWhite, adjacentRow.Value.ClearWhite); ImGui.PopID(); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_green"); - if (this.DrawColorColumn(row.Unknown1) && + if (this.DrawColorColumn(row.ClearGreen) && adjacentRow.HasValue) - DrawEdgePreview(id, row.Unknown1, adjacentRow.Value.Unknown1); + DrawEdgePreview(id, row.ClearGreen, adjacentRow.Value.ClearGreen); ImGui.PopID(); } } From fc130e325cf967069e8c0b0b9026dd3833af42fc Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 26 Dec 2025 16:15:58 +0100 Subject: [PATCH 5/8] Fix warnings --- Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs index 4d858922a..56ed45446 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -25,9 +25,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class UldWidget : IDataWindowWidget { + private const string UldBaseBath = "ui/uld/"; + // ULD styles can be hardcoded for now as they don't add new ones regularly. Can later try and find where to load these from in the game EXE. private static readonly string[] ThemeDisplayNames = ["Dark", "Light", "Classic FF", "Clear Blue", "Clear White", "Clear Green"]; - private const string UldBaseBath = "ui/uld/"; // 48 8D 15 ?? ?? ?? ?? is the part of the signatures that contain the string location offset // 48 = 64 bit register prefix @@ -263,7 +264,7 @@ internal class UldWidget : IDataWindowWidget } private string ToThemedPath(string path) => - UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : "") + path[UldBaseBath.Length..]; + UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : string.Empty) + path[UldBaseBath.Length..]; private void DrawTextureEntry(UldRoot.TextureEntry textureEntry, TextureManager textureManager) { From 682b932a116ee82a227e9e12dbad234e7e52aa26 Mon Sep 17 00:00:00 2001 From: Jerric <58122205+Ivinedra@users.noreply.github.com> Date: Thu, 25 Dec 2025 01:46:25 -0500 Subject: [PATCH 6/8] Additional installed plugin categories Introduces new plugin categories for enabled, disabled, and incompatible plugins in PluginCategoryManager and PluginInstallerWindow. Updates filtering logic, UI strings, and groupings to support these new categories, allowing users to view plugins by their enabled/disabled/incompatible status. --- .../Internal/PluginCategoryManager.cs | 26 ++++++++++++++- .../PluginInstaller/PluginInstallerWindow.cs | 33 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs index d3aea7f57..c25f55634 100644 --- a/Dalamud/Interface/Internal/PluginCategoryManager.cs +++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs @@ -30,6 +30,9 @@ internal class PluginCategoryManager new(CategoryKind.PluginChangelogs, "special.plugins", () => Locs.Category_Plugins), new(CategoryKind.PluginProfiles, "special.profiles", () => Locs.Category_PluginProfiles), new(CategoryKind.UpdateablePlugins, "special.updateable", () => Locs.Category_UpdateablePlugins), + new(CategoryKind.EnabledPlugins, "special.enabled", () => Locs.Category_EnabledPlugins), + new(CategoryKind.DisabledPlugins, "special.disabled", () => Locs.Category_DisabledPlugins), + new(CategoryKind.IncompatiblePlugins, "special.incompatible", () => Locs.Category_IncompatiblePlugins), // Tag-driven categories new(CategoryKind.Other, "other", () => Locs.Category_Other), @@ -47,7 +50,7 @@ internal class PluginCategoryManager private GroupInfo[] groupList = [ new(GroupKind.DevTools, () => Locs.Group_DevTools, CategoryKind.DevInstalled, CategoryKind.IconTester), - new(GroupKind.Installed, () => Locs.Group_Installed, CategoryKind.All, CategoryKind.IsTesting, CategoryKind.UpdateablePlugins, CategoryKind.PluginProfiles), + new(GroupKind.Installed, () => Locs.Group_Installed, CategoryKind.All, CategoryKind.IsTesting, CategoryKind.UpdateablePlugins, CategoryKind.PluginProfiles, CategoryKind.EnabledPlugins, CategoryKind.DisabledPlugins, CategoryKind.IncompatiblePlugins), new(GroupKind.Available, () => Locs.Group_Available, CategoryKind.All), new(GroupKind.Changelog, () => Locs.Group_Changelog, CategoryKind.All, CategoryKind.DalamudChangelogs, CategoryKind.PluginChangelogs) @@ -142,6 +145,21 @@ internal class PluginCategoryManager /// UpdateablePlugins = 15, + /// + /// Enabled plugins. + /// + EnabledPlugins = 16, + + /// + /// Disabled plugins. + /// + DisabledPlugins = 17, + + /// + /// Incompatible plugins. + /// + IncompatiblePlugins = 18, + /// /// Plugins tagged as "other". /// @@ -555,6 +573,12 @@ internal class PluginCategoryManager public static string Category_PluginProfiles => Loc.Localize("InstallerCategoryPluginProfiles", "Plugin Collections"); public static string Category_UpdateablePlugins => Loc.Localize("InstallerCategoryCanBeUpdated", "Can be updated"); + + public static string Category_EnabledPlugins => Loc.Localize("InstallerCategoryEnabledPlugins", "Enabled"); + + public static string Category_DisabledPlugins => Loc.Localize("InstallerCategoryDisabledPlugins", "Disabled"); + + public static string Category_IncompatiblePlugins => Loc.Localize("InstallerCategoryIncompatiblePlugins", "Incompatible"); public static string Category_Other => Loc.Localize("InstallerCategoryOther", "Other"); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 3241015fc..72fed787d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -235,6 +235,9 @@ internal class PluginInstallerWindow : Window, IDisposable Testing, Updateable, Dev, + Enabled, + Disabled, + Incompatible, } private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress || @@ -1453,6 +1456,15 @@ internal class PluginInstallerWindow : Window, IDisposable if (filter == InstalledPluginListFilter.Testing && !manager.HasTestingOptIn(plugin.Manifest)) continue; + if (filter == InstalledPluginListFilter.Enabled && (!plugin.IsWantedByAnyProfile || plugin.IsOutdated || plugin.IsBanned || plugin.IsOrphaned || plugin.IsDecommissioned)) + continue; + + if (filter == InstalledPluginListFilter.Disabled && (plugin.IsWantedByAnyProfile || plugin.IsOutdated || plugin.IsBanned || plugin.IsOrphaned || plugin.IsDecommissioned)) + continue; + + if (filter == InstalledPluginListFilter.Incompatible && !(plugin.IsOutdated || plugin.IsBanned || plugin.IsOrphaned || plugin.IsDecommissioned)) + continue; + // Find applicable update and manifest, if we have them AvailablePluginUpdate? update = null; RemotePluginManifest? remoteManifest = null; @@ -1485,6 +1497,9 @@ internal class PluginInstallerWindow : Window, IDisposable InstalledPluginListFilter.Testing => Locs.TabBody_NoPluginsTesting, InstalledPluginListFilter.Updateable => Locs.TabBody_NoPluginsUpdateable, InstalledPluginListFilter.Dev => Locs.TabBody_NoPluginsDev, + InstalledPluginListFilter.Enabled => Locs.TabBody_NoPluginsEnabled, + InstalledPluginListFilter.Disabled => Locs.TabBody_NoPluginsDisabled, + InstalledPluginListFilter.Incompatible => Locs.TabBody_NoPluginsIncompatible, _ => throw new ArgumentException(null, nameof(filter)), }; @@ -1725,6 +1740,18 @@ internal class PluginInstallerWindow : Window, IDisposable this.DrawInstalledPluginList(InstalledPluginListFilter.Updateable); break; + case PluginCategoryManager.CategoryKind.EnabledPlugins: + this.DrawInstalledPluginList(InstalledPluginListFilter.Enabled); + break; + + case PluginCategoryManager.CategoryKind.DisabledPlugins: + this.DrawInstalledPluginList(InstalledPluginListFilter.Disabled); + break; + + case PluginCategoryManager.CategoryKind.IncompatiblePlugins: + this.DrawInstalledPluginList(InstalledPluginListFilter.Incompatible); + break; + case PluginCategoryManager.CategoryKind.PluginProfiles: this.profileManagerWidget.Draw(); break; @@ -4097,6 +4124,12 @@ internal class PluginInstallerWindow : Window, IDisposable public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them from the settings."); + public static string TabBody_NoPluginsEnabled => Loc.Localize("InstallerNoPluginsEnabled", "You don't have any enabled plugins."); + + public static string TabBody_NoPluginsDisabled => Loc.Localize("InstallerNoPluginsDisabled", "You don't have any disabled plugins."); + + public static string TabBody_NoPluginsIncompatible => Loc.Localize("InstallerNoPluginsIncompatible", "You don't have any incompatible plugins."); + #endregion #region Search text From 97748072f5327c7d8bf7084f54d1a84af35b244c Mon Sep 17 00:00:00 2001 From: Jerric <58122205+Ivinedra@users.noreply.github.com> Date: Thu, 25 Dec 2025 14:42:09 -0500 Subject: [PATCH 7/8] Add Repository Filter (InstallerWindow) Introduces a repository filter dropdown to the PluginInstallerWindow, allowing users to filter plugins by their source repository. --- .../PluginInstaller/PluginInstallerWindow.cs | 192 +++++++++++++++++- 1 file changed, 186 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 72fed787d..8fe304fab 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -126,6 +126,11 @@ internal class PluginInstallerWindow : Window, IDisposable private string filterText = Locs.SortBy_Alphabetical; private bool adaptiveSort = true; + private string? selectedRepoUrl = null; // null = All Repositories + private List cachedRepoUrls = new(); + private HashSet cachedRepoUrlsNormalized = new(StringComparer.OrdinalIgnoreCase); + private const string XivLauncherRepoKey = "XIVLauncher"; + private OperationStatus installStatus = OperationStatus.Idle; private OperationStatus updateStatus = OperationStatus.Idle; @@ -789,7 +794,64 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.EndCombo(); } } - } + + // Repository filter (overarching filter for repositories) + this.RefreshRepoFilterList(); + + // Disabled for changelog views or profile editor (plugin collections). + var disableRepoFilter = this.categoryManager.CurrentGroupKind == PluginCategoryManager.GroupKind.Changelog || isProfileManager; + + using (ImRaii.Disabled(disableRepoFilter)) + { + ImGuiHelpers.ScaledDummy(5); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Repository Filter"); + ImGui.SameLine(); + + // Fill remaining width of the header row + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + + var repoPreview = this.selectedRepoUrl ?? "All Repositories"; + if (ImGui.BeginCombo("##RepoFilterCombo", repoPreview)) + { + if (ImGui.Selectable("All Repositories", this.selectedRepoUrl == null)) + { + this.selectedRepoUrl = null; + this.UpdateCategoriesOnPluginsChange(); + this.openPluginCollapsibles.Clear(); + } + + if (ImGui.Selectable("XIVLauncher", this.selectedRepoUrl == XivLauncherRepoKey)) + { + this.selectedRepoUrl = XivLauncherRepoKey; + this.UpdateCategoriesOnPluginsChange(); + this.openPluginCollapsibles.Clear(); + } + + ImGui.Separator(); + + for (var i = 0; i < this.cachedRepoUrls.Count; i++) + { + var repoUrl = this.cachedRepoUrls[i]; + var selected = this.selectedRepoUrl == repoUrl; + + // Use stable unique IDs to avoid ImGui collisions with long/duplicate URLs. + if (ImGui.Selectable($"{repoUrl}##repo_{i}", selected)) + { + this.selectedRepoUrl = repoUrl; + this.UpdateCategoriesOnPluginsChange(); + this.openPluginCollapsibles.Clear(); + } + } + + ImGui.EndCombo(); + } + + ImGuiHelpers.ScaledDummy(10); + } + +} private void DrawFooter() { @@ -1306,6 +1368,7 @@ internal class PluginInstallerWindow : Window, IDisposable var filteredAvailableManifests = availableManifests .Where(rm => !this.IsManifestFiltered(rm)) + .Where(this.PassesRepoFilter) .ToList(); if (filteredAvailableManifests.Count == 0) @@ -1318,7 +1381,7 @@ internal class PluginInstallerWindow : Window, IDisposable { var plugin = this.pluginListInstalled .FirstOrDefault(plugin => plugin.Manifest.InternalName == availableManifest.InternalName && - plugin.Manifest.RepoUrl == availableManifest.RepoUrl && + RepoUrlMatches(GetRepoFilterUrl(plugin), GetRepoFilterUrl(availableManifest)) && !plugin.IsDev); // We "consumed" this plugin from the pile and remove it. @@ -1339,6 +1402,9 @@ internal class PluginInstallerWindow : Window, IDisposable if (this.IsManifestFiltered(installedPlugin.Manifest)) continue; + if (!this.PassesRepoFilter(GetRepoFilterUrl(installedPlugin))) + continue; + // TODO: We should also check categories here, for good measure proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin)); @@ -1441,6 +1507,7 @@ internal class PluginInstallerWindow : Window, IDisposable var filteredList = pluginList .Where(plugin => !this.IsManifestFiltered(plugin.Manifest)) + .Where(plugin => this.PassesRepoFilter(!string.IsNullOrWhiteSpace(plugin.Manifest.InstalledFromUrl) ? plugin.Manifest.InstalledFromUrl : plugin.Manifest.RepoUrl)) .ToList(); if (filteredList.Count == 0) @@ -1478,7 +1545,7 @@ internal class PluginInstallerWindow : Window, IDisposable // Find the applicable remote manifest remoteManifest = this.pluginListAvailable .FirstOrDefault(rm => rm.InternalName == plugin.Manifest.InternalName && - rm.RepoUrl == plugin.Manifest.RepoUrl); + RepoUrlMatches(rm.RepoUrl, plugin.Manifest.RepoUrl)); } else if (!plugin.IsDev) { @@ -3976,12 +4043,12 @@ internal class PluginInstallerWindow : Window, IDisposable } else { - var pluginsMatchingSearch = this.pluginListAvailable.Where(rm => !this.IsManifestFiltered(rm)).ToArray(); + var pluginsMatchingSearch = this.pluginListAvailable.Where(rm => this.PassesRepoFilter(rm) && !this.IsManifestFiltered(rm)).ToArray(); // Check if the search results are different, and clear the open collapsibles if they are if (previousSearchText != null) { - var previousSearchResults = this.pluginListAvailable.Where(rm => !this.IsManifestFiltered(rm)).ToArray(); + var previousSearchResults = this.pluginListAvailable.Where(rm => this.PassesRepoFilter(rm) && !this.IsManifestFiltered(rm)).ToArray(); if (!previousSearchResults.SequenceEqual(pluginsMatchingSearch)) this.openPluginCollapsibles.Clear(); } @@ -3992,10 +4059,123 @@ internal class PluginInstallerWindow : Window, IDisposable private void UpdateCategoriesOnPluginsChange() { - this.categoryManager.BuildCategories(this.pluginListAvailable); + this.categoryManager.BuildCategories(this.pluginListAvailable.Where(this.PassesRepoFilter).ToList()); this.UpdateCategoriesOnSearchChange(null); } + private void RefreshRepoFilterList() + { + var config = Service.Get(); + + this.cachedRepoUrls.Clear(); + this.cachedRepoUrlsNormalized.Clear(); + + // Main repo is represented by the "XIVLauncher" entry; here we only list third-party repos. + var seenDisplay = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var repo in config.ThirdRepoList) + { + if (string.IsNullOrWhiteSpace(repo.Url)) + continue; + + var trimmed = repo.Url.Trim(); + if (!seenDisplay.Add(trimmed)) + continue; + + this.cachedRepoUrls.Add(trimmed); + + var normalized = NormalizeRepoUrl(trimmed); + if (!string.IsNullOrEmpty(normalized)) + this.cachedRepoUrlsNormalized.Add(normalized); + } + } + + + private static string NormalizeRepoUrl(string? url) + { + if (string.IsNullOrWhiteSpace(url)) + return string.Empty; + + url = url.Trim(); + + // Best-effort URI normalization: ignore query/fragment; compare by path. + if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + var left = uri.GetLeftPart(UriPartial.Path); + return left.TrimEnd('/'); + } + + return url.TrimEnd('/'); + } + + private static bool RepoUrlMatches(string? a, string? b) + { + var na = NormalizeRepoUrl(a); + var nb = NormalizeRepoUrl(b); + + if (string.IsNullOrEmpty(na) || string.IsNullOrEmpty(nb)) + return false; + + if (string.Equals(na, nb, StringComparison.OrdinalIgnoreCase)) + return true; + + // Allow prefix matches to handle minor formatting differences (e.g. /api/6 suffix). + // Ensure the prefix boundary is a path separator. + if (na.Length < nb.Length && nb.StartsWith(na, StringComparison.OrdinalIgnoreCase)) + return nb[na.Length] == '/'; + + if (nb.Length < na.Length && na.StartsWith(nb, StringComparison.OrdinalIgnoreCase)) + return na[nb.Length] == '/'; + + return false; + } + + + private static string? GetRepoFilterUrl(RemotePluginManifest manifest) +{ + // For available manifests, RepoUrl is often the plugin's project URL (or null). + // The repository identity we care about is the *source repo* (pluginmaster URL) vs XIVLauncher (main repo). + if (manifest.SourceRepo != null && manifest.SourceRepo.IsThirdParty) + return manifest.SourceRepo.PluginMasterUrl; + + return PluginRepository.MainRepoUrl; +} + + private static string? GetRepoFilterUrl(LocalPlugin plugin) +{ + // Installed third-party plugins store their origin in InstalledFromUrl. + if (plugin.IsThirdParty) + return plugin.Manifest.InstalledFromUrl; + + return PluginRepository.MainRepoUrl; +} + + private bool PassesRepoFilter(string? repoUrl) + { + // null => All Repositories + if (this.selectedRepoUrl == null) + return true; + + // Anything not from the user's third-party repo list + if (this.selectedRepoUrl == XivLauncherRepoKey) + { + // Treat empty and the known main repo URL as "XIVLauncher" + if (string.IsNullOrWhiteSpace(repoUrl) || RepoUrlMatches(repoUrl, PluginRepository.MainRepoUrl)) + return true; + + var normalized = NormalizeRepoUrl(repoUrl); + if (string.IsNullOrEmpty(normalized)) + return true; + + // Anything not matching the configured third-party repos is considered "XIVLauncher". + return !this.cachedRepoUrlsNormalized.Any(u => RepoUrlMatches(u, normalized) || string.Equals(u, normalized, StringComparison.OrdinalIgnoreCase)); + } + + return RepoUrlMatches(repoUrl, this.selectedRepoUrl); + } + + private bool PassesRepoFilter(RemotePluginManifest manifest) => this.PassesRepoFilter(GetRepoFilterUrl(manifest)); + private void DrawFontawesomeIconOutlined(FontAwesomeIcon icon, Vector4 outline, Vector4 iconColor) { var positionOffset = ImGuiHelpers.ScaledVector2(0.0f, 1.0f); From 46a5b8276a48585f7f6486871800b6021cffd362 Mon Sep 17 00:00:00 2001 From: Jerric <58122205+Ivinedra@users.noreply.github.com> Date: Thu, 25 Dec 2025 15:10:30 -0500 Subject: [PATCH 8/8] Optimization of PluginInstallerWindow Refactored repository selection logic. Reduced code duplication and improving maintainability. Added caching for third-party repo list to avoid unnecessary rebuilds. Updated repo filter logic to use normalized URLs for more reliable matching. --- .../PluginInstaller/PluginInstallerWindow.cs | 69 +++++++++++++------ 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 8fe304fab..aeb1d0adb 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -127,11 +127,26 @@ internal class PluginInstallerWindow : Window, IDisposable private bool adaptiveSort = true; private string? selectedRepoUrl = null; // null = All Repositories - private List cachedRepoUrls = new(); - private HashSet cachedRepoUrlsNormalized = new(StringComparer.OrdinalIgnoreCase); + private string selectedRepoUrlNormalized = string.Empty; + private int repoFilterCacheKey = 0; + + private readonly List cachedRepoUrls = new(); + private readonly HashSet cachedRepoUrlsNormalized = new(StringComparer.OrdinalIgnoreCase); private const string XivLauncherRepoKey = "XIVLauncher"; - private OperationStatus installStatus = OperationStatus.Idle; + private void SetSelectedRepo(string? repoUrl) + { + if (this.selectedRepoUrl == repoUrl) + return; + + this.selectedRepoUrl = repoUrl; + this.selectedRepoUrlNormalized = NormalizeRepoUrl(repoUrl); + + this.UpdateCategoriesOnPluginsChange(); + this.openPluginCollapsibles.Clear(); + } + +private OperationStatus installStatus = OperationStatus.Idle; private OperationStatus updateStatus = OperationStatus.Idle; private OperationStatus enableDisableStatus = OperationStatus.Idle; @@ -817,17 +832,13 @@ internal class PluginInstallerWindow : Window, IDisposable { if (ImGui.Selectable("All Repositories", this.selectedRepoUrl == null)) { - this.selectedRepoUrl = null; - this.UpdateCategoriesOnPluginsChange(); - this.openPluginCollapsibles.Clear(); - } + this.SetSelectedRepo(null); +} if (ImGui.Selectable("XIVLauncher", this.selectedRepoUrl == XivLauncherRepoKey)) { - this.selectedRepoUrl = XivLauncherRepoKey; - this.UpdateCategoriesOnPluginsChange(); - this.openPluginCollapsibles.Clear(); - } + this.SetSelectedRepo(XivLauncherRepoKey); +} ImGui.Separator(); @@ -839,10 +850,8 @@ internal class PluginInstallerWindow : Window, IDisposable // Use stable unique IDs to avoid ImGui collisions with long/duplicate URLs. if (ImGui.Selectable($"{repoUrl}##repo_{i}", selected)) { - this.selectedRepoUrl = repoUrl; - this.UpdateCategoriesOnPluginsChange(); - this.openPluginCollapsibles.Clear(); - } + this.SetSelectedRepo(repoUrl); +} } ImGui.EndCombo(); @@ -4067,7 +4076,25 @@ internal class PluginInstallerWindow : Window, IDisposable { var config = Service.Get(); - this.cachedRepoUrls.Clear(); + // Avoid rebuilding every frame unless the repo list actually changed. + var newKey = 17; + unchecked + { + foreach (var repo in config.ThirdRepoList) + { + if (string.IsNullOrWhiteSpace(repo.Url)) + continue; + + newKey = (newKey * 31) + StringComparer.OrdinalIgnoreCase.GetHashCode(repo.Url.Trim()); + } + } + + if (newKey == this.repoFilterCacheKey) + return; + + this.repoFilterCacheKey = newKey; + +this.cachedRepoUrls.Clear(); this.cachedRepoUrlsNormalized.Clear(); // Main repo is represented by the "XIVLauncher" entry; here we only list third-party repos. @@ -4131,7 +4158,7 @@ internal class PluginInstallerWindow : Window, IDisposable } - private static string? GetRepoFilterUrl(RemotePluginManifest manifest) +private static string? GetRepoFilterUrl(RemotePluginManifest manifest) { // For available manifests, RepoUrl is often the plugin's project URL (or null). // The repository identity we care about is the *source repo* (pluginmaster URL) vs XIVLauncher (main repo). @@ -4141,7 +4168,7 @@ internal class PluginInstallerWindow : Window, IDisposable return PluginRepository.MainRepoUrl; } - private static string? GetRepoFilterUrl(LocalPlugin plugin) +private static string? GetRepoFilterUrl(LocalPlugin plugin) { // Installed third-party plugins store their origin in InstalledFromUrl. if (plugin.IsThirdParty) @@ -4168,15 +4195,15 @@ internal class PluginInstallerWindow : Window, IDisposable return true; // Anything not matching the configured third-party repos is considered "XIVLauncher". - return !this.cachedRepoUrlsNormalized.Any(u => RepoUrlMatches(u, normalized) || string.Equals(u, normalized, StringComparison.OrdinalIgnoreCase)); + return !this.cachedRepoUrlsNormalized.Contains(normalized); } - return RepoUrlMatches(repoUrl, this.selectedRepoUrl); + return RepoUrlMatches(repoUrl, this.selectedRepoUrlNormalized); } private bool PassesRepoFilter(RemotePluginManifest manifest) => this.PassesRepoFilter(GetRepoFilterUrl(manifest)); - private void DrawFontawesomeIconOutlined(FontAwesomeIcon icon, Vector4 outline, Vector4 iconColor) +private void DrawFontawesomeIconOutlined(FontAwesomeIcon icon, Vector4 outline, Vector4 iconColor) { var positionOffset = ImGuiHelpers.ScaledVector2(0.0f, 1.0f); var cursorStart = ImGui.GetCursorPos() + positionOffset;