mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-24 21:51:49 +01:00
Add Repository Filter (InstallerWindow)
Introduces a repository filter dropdown to the PluginInstallerWindow, allowing users to filter plugins by their source repository.
This commit is contained in:
parent
419601d9d1
commit
2539d330ac
1 changed files with 186 additions and 6 deletions
|
|
@ -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<string> cachedRepoUrls = new();
|
||||
private HashSet<string> 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<DalamudConfiguration>.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<string>(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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue