diff --git a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs index d4cde5ac28..49ebd8a484 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs @@ -618,7 +618,10 @@ partial void OnGlobalQueryTextChanged(string value) { MegaQueryText = ""; MegaQueryVisible = true; - Loader?.ClearPackages(emitFinishSignal: false); + if (Loader is DiscoverablePackagesLoader discoverLoader) + discoverLoader.ResetSearch(); + else + Loader?.ClearPackages(emitFinishSignal: false); } else { @@ -677,7 +680,7 @@ private Func BuildPackageFilter(out string query) foreach (var f in filters) normalizedQuery = f(normalizedQuery); query = normalizedQuery; - Func matchFunc = SearchMode switch + Func matchFunc = normalizedQuery.Length == 0 ? _ => true : SearchMode switch { SearchMode.Name => pkg => FilterHelpers.NameContains(pkg, normalizedQuery, filters), SearchMode.Id => pkg => FilterHelpers.IdContains(pkg, normalizedQuery, filters), @@ -719,7 +722,7 @@ private void UpdateFilteredState(string query) { bool noQuery = string.IsNullOrWhiteSpace(query); BackgroundText = noQuery ? NoPackagesText : NoMatchesText; - BackgroundTextVisible = !MegaQueryBoxEnabled || !noQuery; + BackgroundTextVisible = !MegaQueryBoxEnabled || !noQuery || !MegaQueryVisible; LoadingImageVisible = false; NoPackagesImageVisible = noQuery && BackgroundTextVisible && NoPackagesImage is not null; } diff --git a/src/UniGetUI.PackageEngine.Enums/Enums.cs b/src/UniGetUI.PackageEngine.Enums/Enums.cs index 04994c4532..5180464dd3 100644 --- a/src/UniGetUI.PackageEngine.Enums/Enums.cs +++ b/src/UniGetUI.PackageEngine.Enums/Enums.cs @@ -1,5 +1,10 @@ namespace UniGetUI.PackageEngine.Enums { + public static class SearchLimits + { + public const int MaxEmptyQueryResultsPerSource = 100; + } + /// /// Represents the installation scope of a package /// diff --git a/src/UniGetUI.PackageEngine.Enums/ManagerCapabilities.cs b/src/UniGetUI.PackageEngine.Enums/ManagerCapabilities.cs index 9f2e32d9b5..b9428ab04c 100644 --- a/src/UniGetUI.PackageEngine.Enums/ManagerCapabilities.cs +++ b/src/UniGetUI.PackageEngine.Enums/ManagerCapabilities.cs @@ -43,6 +43,7 @@ public struct ManagerCapabilities public bool SupportsCustomScopesOnUpdate = true; public bool SupportsCustomScopesOnUninstall = true; public bool SupportsPreRelease = false; + public bool SupportsEmptySearchQuery = false; public bool SerializesOperations = false; public bool SupportsCustomLocations = false; public bool SupportsCustomSources = false; diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs index 626dbb02c1..70fc93337d 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/NativeWinGetHelper.cs @@ -184,7 +184,7 @@ public IReadOnlyList FindPackages_UnSafe(string query) List packages = []; INativeTaskLogger logger = Manager.TaskLogger.CreateNew(LoggableTaskType.FindPackages); Dictionary< - (PackageCatalogReference, PackageMatchField), + (PackageCatalogReference, PackageMatchField?), Task > FindPackageTasks = []; @@ -203,24 +203,27 @@ public IReadOnlyList FindPackages_UnSafe(string query) ConnectResult result = CatalogReference.Connect(); if (result.Status == ConnectResultStatus.Ok) { - foreach ( - var filter_type in new[] - { - PackageMatchField.Name, - PackageMatchField.Id, - PackageMatchField.Moniker, - } - ) + PackageMatchField?[] filterTypes = query.Length == 0 + ? [null] + : [PackageMatchField.Name, PackageMatchField.Id, PackageMatchField.Moniker]; + foreach (PackageMatchField? filter_type in filterTypes) { FindPackagesOptions PackageFilters = Factory.CreateFindPackagesOptions(); - logger.Log("Generating filters..."); - // Name filter - PackageMatchFilter FilterName = Factory.CreatePackageMatchFilter(); - FilterName.Field = filter_type; - FilterName.Value = query; - FilterName.Option = PackageFieldMatchOption.ContainsCaseInsensitive; - PackageFilters.Filters.Add(FilterName); + if (filter_type is { } field) + { + logger.Log("Generating filters..."); + // Name filter + PackageMatchFilter FilterName = Factory.CreatePackageMatchFilter(); + FilterName.Field = field; + FilterName.Value = query; + FilterName.Option = PackageFieldMatchOption.ContainsCaseInsensitive; + PackageFilters.Filters.Add(FilterName); + } + else + { + PackageFilters.ResultLimit = (uint)SearchLimits.MaxEmptyQueryResultsPerSource; + } try { diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetCliHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetCliHelper.cs index 20c30cfc30..51dcc295f3 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetCliHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/PingetCliHelper.cs @@ -113,10 +113,35 @@ public IReadOnlyList GetInstalledPackages_UnSafe() public IReadOnlyList FindPackages_UnSafe(string query) { - SearchResponse result = RunJson( - LoggableTaskType.FindPackages, - $"search {Quote(query)} --output json" - ); + if (query.Length == 0) + { + return Manager + .SourcesHelper.GetSources() + .SelectMany(SearchSourceForBrowse) + .ToArray(); + } + + return SearchPackages($"search {Quote(query)} --output json"); + } + + private Package[] SearchSourceForBrowse(IManagerSource source) + { + try + { + return SearchPackages( + $"search {Quote(string.Empty)} --source {Quote(source.Name)} --count {SearchLimits.MaxEmptyQueryResultsPerSource} --output json" + ); + } + catch (Exception ex) + { + Logger.Warn($"Could not list the packages of the WinGet source {source.Name}: {ex.Message}"); + return []; + } + } + + private Package[] SearchPackages(string arguments) + { + SearchResponse result = RunJson(LoggableTaskType.FindPackages, arguments); return result .Matches.Select(match => diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/WinGetCliHelper.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/WinGetCliHelper.cs index f6f581ce07..a6c92f79e1 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/WinGetCliHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/ClientHelpers/WinGetCliHelper.cs @@ -1,5 +1,7 @@ using System.Diagnostics; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; using UniGetUI.Core.Logging; using UniGetUI.Core.Tools; @@ -11,7 +13,7 @@ namespace UniGetUI.PackageEngine.Managers.WingetManager; -internal sealed class WinGetCliHelper : IWinGetManagerHelper +internal sealed partial class WinGetCliHelper : IWinGetManagerHelper { // "winget search a" returns ~12k results; cap to the most relevant to avoid the freeze/RAM spike. private const int MAX_SEARCH_RESULTS = 100; @@ -270,6 +272,80 @@ IEnumerable outputLines } public IReadOnlyList FindPackages_UnSafe(string query) + { + if (query.Length == 0) + { + return GetSourcesForBrowse() + .SelectMany(source => SearchPackages(query, source, SearchLimits.MaxEmptyQueryResultsPerSource)) + .ToArray(); + } + + return SearchPackages(query, null, MAX_SEARCH_RESULTS); + } + + private List GetSourcesForBrowse() + { + using var _cliLock = WinGet.AcquireCliLock(); + List sources = []; + using Process p = new() + { + StartInfo = new() + { + FileName = _cliExecutablePath, + Arguments = Manager.Status.ExecutableCallArgs + " source export", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + }, + }; + + IProcessTaskLogger logger = Manager.TaskLogger.CreateNew(LoggableTaskType.ListSources, p); + + if (CoreTools.IsAdministrator()) + { + string WinGetTemp = AppPaths.RedirectTempToElevatedDirectory(p.StartInfo); + logger.AddToStdErr( + $"[WARN] Redirecting %TEMP% folder to {WinGetTemp}, since UniGetUI was run as admin" + ); + } + + p.Start(); + + foreach (string name in ParseExportedSourceNames(ReadOutputLines(p, logger))) + { + sources.Add(Manager.SourcesHelper.Factory.GetSourceOrDefault(name)); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + return sources; + } + + internal static IEnumerable ParseExportedSourceNames(IEnumerable outputLines) + { + foreach (string line in outputLines) + { + string? name; + try + { + name = JsonSerializer.Deserialize(line, WinGetCliJsonContext.Default.WinGetExportedSource)?.Name; + } + catch (JsonException) + { + continue; + } + + if (!string.IsNullOrWhiteSpace(name)) + { + yield return name; + } + } + } + + private List SearchPackages(string query, IManagerSource? source, int count) { using var _cliLock = WinGet.AcquireCliLock(); List Packages = []; @@ -282,8 +358,10 @@ public IReadOnlyList FindPackages_UnSafe(string query) Manager.Status.ExecutableCallArgs + " search \"" + query - + "\" --count " - + MAX_SEARCH_RESULTS + + "\"" + + (source is null ? "" : " --source \"" + source.Name + "\"") + + " --count " + + count + " --accept-source-agreements " + WinGet.GetProxyArgument(), RedirectStandardOutput = true, @@ -306,7 +384,7 @@ public IReadOnlyList FindPackages_UnSafe(string query) p.Start(); - Packages.AddRange(ParseFoundPackages(Manager, ReadOutputLines(p, logger))); + Packages.AddRange(ParseFoundPackages(Manager, ReadOutputLines(p, logger), source)); logger.AddToStdErr(p.StandardError.ReadToEnd()); p.WaitForExit(); @@ -317,7 +395,8 @@ public IReadOnlyList FindPackages_UnSafe(string query) internal static IReadOnlyList ParseFoundPackages( WinGet manager, - IEnumerable outputLines + IEnumerable outputLines, + IManagerSource? queriedSource = null ) { List packages = []; @@ -338,7 +417,7 @@ IEnumerable outputLines IManagerSource source = sourceName.Length == 0 - ? manager.DefaultSource + ? queriedSource ?? manager.DefaultSource : manager.SourcesHelper.Factory.GetSourceOrDefault(sourceName); packages.Add(new Package(name, id, version, source, manager)); @@ -512,4 +591,9 @@ public IReadOnlyList GetSources_UnSafe() logger.Close(p.ExitCode); return sources; } + + internal sealed record WinGetExportedSource(string? Name); + + [JsonSerializable(typeof(WinGetExportedSource))] + internal sealed partial class WinGetCliJsonContext : JsonSerializerContext { } } diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs index 2aa875a1bb..549bb06ca4 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs @@ -128,6 +128,7 @@ public WinGet() SupportsCustomSources = true, SupportsCustomPackageIcons = true, SupportsCustomPackageScreenshots = true, + SupportsEmptySearchQuery = true, Sources = new SourceCapabilities { KnowsPackageCount = false, diff --git a/src/UniGetUI.PackageEngine.PackageLoader/DiscoverablePackagesLoader.cs b/src/UniGetUI.PackageEngine.PackageLoader/DiscoverablePackagesLoader.cs index 95c9b29ad5..8b09029fb2 100644 --- a/src/UniGetUI.PackageEngine.PackageLoader/DiscoverablePackagesLoader.cs +++ b/src/UniGetUI.PackageEngine.PackageLoader/DiscoverablePackagesLoader.cs @@ -1,6 +1,7 @@ using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; using UniGetUI.Interface.Enums; +using UniGetUI.PackageEngine.Enums; using UniGetUI.PackageEngine.Interfaces; namespace UniGetUI.PackageEngine.PackageLoader @@ -13,7 +14,7 @@ public class DiscoverablePackagesLoader : AbstractPackageLoader // tags and renders thousands of packages, freezing the UI and spiking RAM. private const int MAX_RESULTS_PER_MANAGER = 100; - private string QUERY_TEXT = string.Empty; + private string? QUERY_TEXT; private volatile bool _pendingReload; public DiscoverablePackagesLoader(IReadOnlyList managers) @@ -45,9 +46,15 @@ public async Task ReloadPackages(string query) await ReloadPackages(); } + public void ResetSearch() + { + QUERY_TEXT = null; + ClearPackages(emitFinishSignal: false); + } + public override async Task ReloadPackages() { - if (QUERY_TEXT == "") + if (QUERY_TEXT is null) { return; } @@ -76,23 +83,38 @@ public void SetManagerSearched(IPackageManager manager, bool searched) => Settings.SetDictionaryItem(Settings.K.ExcludedSearchManagers, manager.Name, !searched); protected override bool WillQueryAnyManager() - => Managers.Any(manager => manager.IsReady() && IsManagerSearched(manager)); + { + string? query = QUERY_TEXT; + return Managers.Any(manager => + manager.IsReady() && IsManagerSearched(manager) && CanSearch(manager, query)); + } + + private static bool CanSearch(IPackageManager manager, string? query) + => query != string.Empty || manager.Capabilities.SupportsEmptySearchQuery; protected override IReadOnlyList LoadPackagesFromManager(IPackageManager manager) { - if (!IsManagerSearched(manager)) + string? query = QUERY_TEXT; + if (query is null || !IsManagerSearched(manager) || !CanSearch(manager, query)) { return []; } - string text = QUERY_TEXT; - text = CoreTools.EnsureSafeQueryString(text); - if (text == string.Empty) + string text = CoreTools.EnsureSafeQueryString(query); + if (text.Length == 0 && query.Length > 0) { return []; } IReadOnlyList found = manager.FindPackages(text); + if (text.Length == 0) + { + return found + .GroupBy(package => package.Source.Name) + .SelectMany(group => group.Take(SearchLimits.MaxEmptyQueryResultsPerSource)) + .ToArray(); + } + return found.Count > MAX_RESULTS_PER_MANAGER ? found.Take(MAX_RESULTS_PER_MANAGER).ToArray() : found; diff --git a/src/UniGetUI.PackageEngine.Tests/DiscoverablePackagesLoaderTests.cs b/src/UniGetUI.PackageEngine.Tests/DiscoverablePackagesLoaderTests.cs index 87a2c27011..b4b1624310 100644 --- a/src/UniGetUI.PackageEngine.Tests/DiscoverablePackagesLoaderTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/DiscoverablePackagesLoaderTests.cs @@ -143,6 +143,126 @@ public async Task NoSelectedManagerYieldsNoPackagesWithoutQuerying() Assert.False(loader.IsLoading); } + [Fact] + public async Task EmptyQueryOnlyQueriesManagersThatSupportIt() + { + List alphaQueries = []; + int betaCalls = 0; + + var alpha = new PackageManagerBuilder() + .WithName("Alpha") + .WithDisplayName("Alpha") + .ConfigureCapabilities(capabilities => + { + capabilities.SupportsEmptySearchQuery = true; + return capabilities; + }) + .WithFindPackages((m, query) => + { + alphaQueries.Add(query); + return [new PackageBuilder().WithManager(m).WithId("Alpha.Pkg").Build()]; + }) + .Build(); + + var beta = new PackageManagerBuilder() + .WithName("Beta") + .WithDisplayName("Beta") + .WithFindPackages((m, _) => + { + betaCalls++; + return [new PackageBuilder().WithManager(m).WithId("Beta.Pkg").Build()]; + }) + .Build(); + + _ = new InstalledPackagesLoader([alpha, beta]); + _ = new UpgradablePackagesLoader([alpha, beta]); + var loader = new ProbeLoader([alpha, beta]); + + await loader.ReloadPackages(""); + + Assert.Equal([""], alphaQueries); + Assert.Equal(0, betaCalls); + Assert.Equal("Alpha.Pkg", Assert.Single(loader.Packages).Id); + + loader.SetManagerSearched(alpha, false); + Assert.False(loader.WillQuery()); + } + + [Fact] + public async Task EmptyQueryCapsResultsPerSourceSoLaterSourcesAreNotCrowdedOut() + { + var alpha = new PackageManagerBuilder() + .WithName("Alpha") + .WithDisplayName("Alpha") + .ConfigureCapabilities(capabilities => + { + capabilities.SupportsEmptySearchQuery = true; + return capabilities; + }) + .WithFindPackages((m, _) => + { + var publicSource = new SourceBuilder().WithManager(m).WithName("public").Build(); + var privateSource = new SourceBuilder().WithManager(m).WithName("private").Build(); + return Enumerable.Range(0, 150) + .Select(i => new PackageBuilder().WithManager(m).WithSource(publicSource).WithId($"Public.{i}").Build()) + .Concat(Enumerable.Range(0, 5) + .Select(i => new PackageBuilder().WithManager(m).WithSource(privateSource).WithId($"Private.{i}").Build())) + .ToArray(); + }) + .Build(); + + _ = new InstalledPackagesLoader([alpha]); + _ = new UpgradablePackagesLoader([alpha]); + var loader = new DiscoverablePackagesLoader([alpha]); + + await loader.ReloadPackages(""); + Assert.Equal(105, loader.Packages.Count); + Assert.Equal(5, loader.Packages.Count(package => package.Source.Name == "private")); + + await loader.ReloadPackages("pkg"); + Assert.Equal(100, loader.Packages.Count); + } + + [Fact] + public async Task NothingIsQueriedBeforeASearchOrForAnUnsafeOnlyQuery() + { + int alphaCalls = 0; + var alpha = new PackageManagerBuilder() + .WithName("Alpha") + .WithDisplayName("Alpha") + .ConfigureCapabilities(capabilities => + { + capabilities.SupportsEmptySearchQuery = true; + return capabilities; + }) + .WithFindPackages((m, _) => + { + alphaCalls++; + return [new PackageBuilder().WithManager(m).WithId("Alpha.Pkg").Build()]; + }) + .Build(); + + _ = new InstalledPackagesLoader([alpha]); + _ = new UpgradablePackagesLoader([alpha]); + var loader = new DiscoverablePackagesLoader([alpha]); + + await loader.ReloadPackages(); + Assert.Equal(0, alphaCalls); + + await loader.ReloadPackages(";|&"); + Assert.Equal(0, alphaCalls); + Assert.Empty(loader.Packages); + + await loader.ReloadPackages(""); + Assert.Equal(1, alphaCalls); + + loader.ResetSearch(); + Assert.Empty(loader.Packages); + await loader.ReloadPackages(); + Assert.Equal(1, alphaCalls); + Assert.Empty(loader.Packages); + } + [Fact] public void SearchScopeIsReadFromAndWrittenToDisk() { diff --git a/src/UniGetUI.PackageEngine.Tests/WinGetCliParsingTests.cs b/src/UniGetUI.PackageEngine.Tests/WinGetCliParsingTests.cs index 4ad5d73ac0..a8b85c52e0 100644 --- a/src/UniGetUI.PackageEngine.Tests/WinGetCliParsingTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/WinGetCliParsingTests.cs @@ -1,5 +1,6 @@ #if WINDOWS using UniGetUI.Core.Data; +using UniGetUI.PackageEngine.Classes.Manager; using UniGetUI.PackageEngine.Managers.WingetManager; using UniGetUI.PackageEngine.PackageClasses; using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions; @@ -286,6 +287,46 @@ VLC UWP 9NBLGGH4VVNH Unknown msstore Assert.Equal("winget", packages[2].Source.Name); } + [Fact] + public void ParseFoundPackagesAssignsTheQueriedSourceWhenTheSourceColumnIsOmitted() + { + var manager = new WinGet(); + var fonts = new ManagerSource(manager, "winget-font", new Uri("https://cdn.winget.microsoft.com/fonts")); + + IReadOnlyList packages = WinGetCliHelper.ParseFoundPackages( + manager, + Lines( + """ + Name Id Version + ------------------------------------------------------------------ + Microsoft Fluent Calibri and Sitka Microsoft.FluentFonts 1.0.0.0 + CaskaydiaCove ryanoasis.CaskaydiaCove 3.4.0 + """ + ), + fonts + ); + + Assert.Equal(2, packages.Count); + PackageAssert.Matches(packages[1], "CaskaydiaCove", "ryanoasis.CaskaydiaCove", "3.4.0"); + Assert.All(packages, package => Assert.Same(fonts, package.Source)); + } + + [Fact] + public void ParseExportedSourceNamesKeepsNamesThatContainSpaces() + { + IEnumerable names = WinGetCliHelper.ParseExportedSourceNames( + [ + """{"Arg":"https://storeedgefd.dsx.mp.microsoft.com/v9.0","Data":"","Explicit":false,"Identifier":"StoreEdgeFD","Name":"msstore","TrustLevel":["Trusted"],"Type":"Microsoft.Rest"}""", + """{"Arg":"https://cdn.winget.microsoft.com/cache","Data":"Microsoft.Winget.Source_8wekyb3d8bbwe","Explicit":false,"Identifier":"Microsoft.Winget.Source_8wekyb3d8bbwe","Name":"winget","TrustLevel":["Trusted","StoreOrigin"],"Type":"Microsoft.PreIndexed.Package"}""", + """{"Arg":"https://packages.contoso.test/api","Data":"","Explicit":true,"Identifier":"Private Feed","Name":"Private Feed","TrustLevel":["None"],"Type":"Microsoft.Rest"}""", + "", + "Failed when opening source(s); try the 'source reset' command if the problem persists.", + ] + ); + + Assert.Equal(["msstore", "winget", "Private Feed"], names); + } + [Fact] public void ParseInstalledPackagesReadsCapturedJapaneseWinGetOutput() { diff --git a/src/UniGetUI.Tui.Tests/E2E/PackagePagesE2ETests.cs b/src/UniGetUI.Tui.Tests/E2E/PackagePagesE2ETests.cs index 9b5ffc11b5..2105f2f92f 100644 --- a/src/UniGetUI.Tui.Tests/E2E/PackagePagesE2ETests.cs +++ b/src/UniGetUI.Tui.Tests/E2E/PackagePagesE2ETests.cs @@ -73,6 +73,20 @@ public async Task DiscoverPage_SearchesAndInstalls() await WaitForTextAsync("Contoso Photo Studio", "17 packages ·"); } + [Test] + public async Task DiscoverPage_EmptySearchListsOnlyManagersThatSupportIt() + { + await ResetAsync(TuiPageIds.Discover); + await EnterPage(); + await Key(K.Enter); + await WaitUntilAsync( + () => DiscoverablePackagesLoader.Instance.IsLoaded && !DiscoverablePackagesLoader.Instance.IsLoading, + "the empty search to finish"); + + NAssert.That(DiscoverablePackagesLoader.Instance.Packages, Is.Not.Empty); + NAssert.That(DiscoverablePackagesLoader.Instance.Packages.Select(p => p.Manager.Name).Distinct(), Is.EquivalentTo(new[] { "Winget" })); + } + [Test] public async Task InstalledPage_UninstallAsksForConfirmation() { diff --git a/src/UniGetUI.Tui/FakeData/FakePackageManager.cs b/src/UniGetUI.Tui/FakeData/FakePackageManager.cs index 9a906c9b0c..88bf33b42d 100644 --- a/src/UniGetUI.Tui/FakeData/FakePackageManager.cs +++ b/src/UniGetUI.Tui/FakeData/FakePackageManager.cs @@ -41,6 +41,7 @@ public FakePackageManager(FakeManagerProfile profile, FakeDataEnvironment env) SupportedCustomArchitectures = profile.SupportsArchitectures ? ["x86", "x64", "arm64"] : [], SupportsCustomScopes = profile.SupportsScopes, SupportsPreRelease = profile.SupportsVersions, + SupportsEmptySearchQuery = profile.Name == "Winget", SupportsCustomLocations = true, SupportsCustomSources = profile.SupportsSources, SupportsCustomPackageIcons = false, diff --git a/src/UniGetUI.Tui/Views/Pages/PackageListPage.cs b/src/UniGetUI.Tui/Views/Pages/PackageListPage.cs index 2958eeb073..f01221b94b 100644 --- a/src/UniGetUI.Tui/Views/Pages/PackageListPage.cs +++ b/src/UniGetUI.Tui/Views/Pages/PackageListPage.cs @@ -182,7 +182,7 @@ internal sealed class PackageListPage : UserControl, ITuiPage private AbstractPackageLoader? _subscribed; private bool _dirty; private DateTime _lastLoadTime = DateTime.Now; - private string _submittedQuery = string.Empty; + private string? _submittedQuery; public PackageListPage(PackagePageKind kind) { @@ -600,7 +600,7 @@ public void Reload() { if (_kind == PackagePageKind.Discover) { - if (_submittedQuery.Length > 0) _ = DiscoverablePackagesLoader.Instance.ReloadPackages(_submittedQuery); + if (_submittedQuery is not null) _ = DiscoverablePackagesLoader.Instance.ReloadPackages(_submittedQuery); return; } @@ -669,14 +669,9 @@ public void SubmitSearch() { string query = (_search.Text ?? string.Empty).Trim(); _submittedQuery = query; - if (query.Length == 0) - { - DiscoverablePackagesLoader.Instance.ClearPackages(emitFinishSignal: false); - Refresh(); - return; - } - - _status.Text = CoreTools.Translate("Searching for \"{0}\"…", query); + _status.Text = query.Length == 0 + ? CoreTools.Translate("Loading packages") + "…" + : CoreTools.Translate("Searching for \"{0}\"…", query); DiscoverablePackagesLoader.Instance.ClearPackages(emitFinishSignal: false); _ = DiscoverablePackagesLoader.Instance.ReloadPackages(query); } @@ -858,7 +853,7 @@ public void Refresh() } string query = (_search.Text ?? string.Empty).Trim(); - if (_kind == PackagePageKind.Discover && query != _submittedQuery) query = _submittedQuery; + if (_kind == PackagePageKind.Discover && query != _submittedQuery) query = _submittedQuery ?? string.Empty; var filtered = snapshot.Where(p => MatchesQuery(p, query) && MatchesSources(p)); List ordered = Sort(filtered).ToList(); @@ -1020,7 +1015,7 @@ private void UpdateStatus() { _status.Text = _kind switch { - PackagePageKind.Discover => _submittedQuery.Length == 0 + PackagePageKind.Discover => _submittedQuery is null ? CoreTools.Translate("Search for packages to start") + "." : CoreTools.Translate("No results were found matching the input criteria"), PackagePageKind.Updates => CoreTools.Translate("Hooray! No updates were found."),