From 0959a9ea482a09277958a0485fe9fffbd4360ad2 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 1/7] 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 2539d330ac8b8e04d0f716b00133b9ec32fac3aa 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 2/7] 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 a563c8355f1c6d3c4e754028d59658d4a10711e9 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 3/7] 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;
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 4/7] 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 5/7] 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 6/7] 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;
From d10422124d123421cf401e462454fd39820b5649 Mon Sep 17 00:00:00 2001
From: Jerric <58122205+Ivinedra@users.noreply.github.com>
Date: Sat, 10 Jan 2026 08:23:10 -0500
Subject: [PATCH 7/7] Small adjustments
-Reorganized categories.
-Disabled Repo filter for Dev Tools.
---
Dalamud/Interface/Internal/PluginCategoryManager.cs | 2 +-
.../Windows/PluginInstaller/PluginInstallerWindow.cs | 8 ++++++--
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs
index 8a3db3bcd..9fdcfe4c1 100644
--- a/Dalamud/Interface/Internal/PluginCategoryManager.cs
+++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs
@@ -51,7 +51,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, CategoryKind.EnabledPlugins, CategoryKind.DisabledPlugins, CategoryKind.IncompatiblePlugins),
+ new(GroupKind.Installed, () => Locs.Group_Installed, CategoryKind.All, CategoryKind.IsTesting, CategoryKind.UpdateablePlugins, CategoryKind.EnabledPlugins, CategoryKind.DisabledPlugins, CategoryKind.IncompatiblePlugins, CategoryKind.PluginProfiles),
new(GroupKind.Available, () => Locs.Group_Available, CategoryKind.All),
new(GroupKind.Changelog, () => Locs.Group_Changelog, CategoryKind.All, CategoryKind.DalamudChangelogs, CategoryKind.PluginChangelogs)
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
index 8c97e8ccb..ae66ff37b 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
@@ -814,8 +814,12 @@ private OperationStatus installStatus = OperationStatus.Idle;
// 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;
+ // Disabled for dev plugin list, changelog views, or profile editor (plugin collections).
+ var disableRepoFilter =
+ isProfileManager ||
+ this.categoryManager.CurrentGroupKind is
+ PluginCategoryManager.GroupKind.Changelog or
+ PluginCategoryManager.GroupKind.DevTools;
using (ImRaii.Disabled(disableRepoFilter))
{