diff --git a/CringeLauncher/CringeLauncher.csproj b/CringeLauncher/CringeLauncher.csproj index 9c2ccb7..0a960ee 100644 --- a/CringeLauncher/CringeLauncher.csproj +++ b/CringeLauncher/CringeLauncher.csproj @@ -18,7 +18,7 @@ - + diff --git a/CringeLauncher/ImGuiHandler.cs b/CringeLauncher/ImGuiHandler.cs index c4b0a96..c8efbee 100644 --- a/CringeLauncher/ImGuiHandler.cs +++ b/CringeLauncher/ImGuiHandler.cs @@ -14,20 +14,18 @@ namespace CringeLauncher; internal class ImGuiHandler : IDisposable { - private readonly DeviceContext _deviceContext; + private DeviceContext? _deviceContext; private static nint _wndproc; public static ImGuiHandler? Instance; public static RenderTargetView? Rtv; - private readonly IRootRenderComponent _renderHandler; + private readonly IRootRenderComponent _renderHandler = new RenderHandler(); - public ImGuiHandler(nint windowHandle, Device1 device, DeviceContext deviceContext) + public void Init(nint windowHandle, Device1 device, DeviceContext deviceContext) { _deviceContext = deviceContext; - _renderHandler = new RenderHandler(); - CreateContext(); var io = GetIO(); @@ -64,7 +62,7 @@ internal class ImGuiHandler : IDisposable Render(); - _deviceContext.ClearState(); + _deviceContext!.ClearState(); _deviceContext.OutputMerger.SetRenderTargets(Rtv); ImGui_ImplDX11_RenderDrawData(GetDrawData()); @@ -87,7 +85,7 @@ internal class ImGuiHandler : IDisposable public void Dispose() { - _deviceContext.Dispose(); + _deviceContext?.Dispose(); _renderHandler.Dispose(); } } \ No newline at end of file diff --git a/CringeLauncher/Launcher.cs b/CringeLauncher/Launcher.cs index 5ab9fc1..f79f493 100644 --- a/CringeLauncher/Launcher.cs +++ b/CringeLauncher/Launcher.cs @@ -101,8 +101,8 @@ public class Launcher : ICorePlugin _renderComponent = new(); _renderComponent.Start(new(), InitEarlyWindow, MyVideoSettingsManager.Initialize(), MyPerGameSettings.MaxFrameRate); _renderComponent.RenderThread.BeforeDraw += MyFpsManager.Update; - _renderComponent.RenderThread.SizeChanged += RenderThreadOnSizeChanged; + // this technically should wait for render thread init, but who cares splash.ExecuteLoadingStages(); InitUgc(); @@ -118,21 +118,20 @@ public class Launcher : ICorePlugin _renderComponent.RenderThread.UpdateSize(); } - private void RenderThreadOnSizeChanged(int width, int height, MyViewport viewport) - { - if (_renderComponent is not null) - _renderComponent.RenderThread.SizeChanged -= RenderThreadOnSizeChanged; - - MyVRage.Platform.Windows.Window.ShowAndFocus(); - } - public void Run() => _game?.Run(); - private static IVRageWindow InitEarlyWindow() + private IVRageWindow InitEarlyWindow() { + ImGuiHandler.Instance = new(); + MyVRage.Platform.Windows.CreateWindow("Cringe Launcher", MyPerGameSettings.GameIcon, null); MyVRage.Platform.Windows.Window.OnExit += MySandboxGame.ExitThreadSafe; + + _renderComponent!.RenderThread.UpdateSize(); + MyRenderProxy.RenderThread = _renderComponent.RenderThread; + + MyVRage.Platform.Windows.Window.ShowAndFocus(); return MyVRage.Platform.Windows.Window; } @@ -157,7 +156,8 @@ public class Launcher : ICorePlugin private static void InitThreadPool() { - ParallelTasks.Parallel.Scheduler = new ThreadPoolScheduler(); + // ParallelTasks.Parallel.Scheduler = new ThreadPoolScheduler(); + MySandboxGame.InitMultithreading(); } private static void ConfigureSettings() diff --git a/CringeLauncher/Patches/SwapChainPatch.cs b/CringeLauncher/Patches/SwapChainPatch.cs index e552dd0..df5238d 100644 --- a/CringeLauncher/Patches/SwapChainPatch.cs +++ b/CringeLauncher/Patches/SwapChainPatch.cs @@ -60,7 +60,7 @@ public static class SwapChainPatch [HarmonyPostfix, HarmonyPatch(typeof(MyRender11), nameof(MyRender11.CreateDeviceInternal))] private static void CreateDevicePostfix() { - ImGuiHandler.Instance ??= new ImGuiHandler(WindowHandle, MyRender11.DeviceInstance, MyRender11.RC.DeviceContext); + ImGuiHandler.Instance?.Init(WindowHandle, MyRender11.DeviceInstance, MyRender11.RC.DeviceContext); } [HarmonyPrefix, HarmonyPatch(typeof(MyBackbuffer), MethodType.Constructor, typeof(SharpDX.Direct3D11.Resource))] diff --git a/CringeLauncher/Utils/ThreadPoolScheduler.cs b/CringeLauncher/Utils/ThreadPoolScheduler.cs index 0a641af..e5e7108 100644 --- a/CringeLauncher/Utils/ThreadPoolScheduler.cs +++ b/CringeLauncher/Utils/ThreadPoolScheduler.cs @@ -8,6 +8,7 @@ using CringeTask = ParallelTasks.Task; namespace CringeLauncher.Utils; +/* public class ThreadPoolScheduler : IWorkScheduler { public void Schedule(CringeTask item) @@ -82,4 +83,4 @@ internal class ThreadPoolWorkItemTask(CringeTask task) : IThreadPoolWorkItem HkBaseSystem.QuitThread(); Debug.WriteLine($"Hk Shutdown for {Thread.CurrentThread.Name}"); } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/CringePlugins/Config/PackagesConfig.cs b/CringePlugins/Config/PackagesConfig.cs index 1da804c..87cf5e6 100644 --- a/CringePlugins/Config/PackagesConfig.cs +++ b/CringePlugins/Config/PackagesConfig.cs @@ -8,9 +8,7 @@ namespace CringePlugins.Config; public record PackagesConfig(ImmutableArray Sources, ImmutableArray Packages) { public static PackagesConfig Default { get; } = new([ - new("SpaceEngineersDedicated.ReferenceAssemblies", "https://ng.zznty.ru/v3/index.json"), - new("ImGui.NET.DirectX", "https://ng.zznty.ru/v3/index.json"), - new("Plugin", "https://ng.zznty.ru/v3/index.json"), + new(@"^SpaceEngineersDedicated\.ReferenceAssemblies$|^ImGui\.NET\.DirectX$|^NuGet$|^Cringe.+$|^SharedCringe$|^Plugin.+$", "https://ng.zznty.ru/v3/index.json"), new(string.Empty, "https://api.nuget.org/v3/index.json") ], []); } \ No newline at end of file diff --git a/CringePlugins/Loader/PluginsLifetime.cs b/CringePlugins/Loader/PluginsLifetime.cs index 88fed83..743252d 100644 --- a/CringePlugins/Loader/PluginsLifetime.cs +++ b/CringePlugins/Loader/PluginsLifetime.cs @@ -2,8 +2,10 @@ using System.Runtime.InteropServices; using System.Text.Json; using CringePlugins.Config; +using CringePlugins.Render; using CringePlugins.Resolver; using CringePlugins.Splash; +using CringePlugins.Ui; using NLog; using NuGet; using NuGet.Deps; @@ -63,6 +65,8 @@ public class PluginsLifetime : ILoadingStage progress.Report("Registering plugins"); RegisterLifetime(); + + RenderHandler.Current.RegisterComponent(new PluginListComponent(packagesConfig, sourceMapping, configPath)); } private void RegisterLifetime() diff --git a/CringePlugins/Render/RenderHandler.cs b/CringePlugins/Render/RenderHandler.cs index 13f6d7c..9d800ef 100644 --- a/CringePlugins/Render/RenderHandler.cs +++ b/CringePlugins/Render/RenderHandler.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using CringePlugins.Abstractions; +using ImGuiNET; using NLog; namespace CringePlugins.Render; @@ -25,6 +26,10 @@ public sealed class RenderHandler : IRootRenderComponent void IRenderComponent.OnFrame() { +#if DEBUG + ImGui.ShowDemoWindow(); +#endif + foreach (var (instanceType, renderComponent) in _components) { try diff --git a/CringePlugins/Resolver/BuiltInPackages.cs b/CringePlugins/Resolver/BuiltInPackages.cs index 577a670..0715e69 100644 --- a/CringePlugins/Resolver/BuiltInPackages.cs +++ b/CringePlugins/Resolver/BuiltInPackages.cs @@ -53,7 +53,12 @@ public static class BuiltInPackages se, imGui, harmony, - FromAssembly(runtimeFramework, [se.AsDependency(), imGui.AsDependency(), harmony.AsDependency()]), + FromAssembly(runtimeFramework, + [se.AsDependency(), imGui.AsDependency(), harmony.AsDependency()] +#if DEBUG + , version: new(0, 1, 4) +#endif + ), ]; } diff --git a/CringePlugins/Resolver/PackageResolver.cs b/CringePlugins/Resolver/PackageResolver.cs index ec0e6ac..90ec8d4 100644 --- a/CringePlugins/Resolver/PackageResolver.cs +++ b/CringePlugins/Resolver/PackageResolver.cs @@ -21,7 +21,7 @@ public class PackageResolver(NuGetFramework runtimeFramework, ImmutableArray - page.Items.Where(b => b.CatalogEntry.PackageTypes is ["CringePlugin"])) + page.Items!.Where(b => b.CatalogEntry.PackageTypes is ["CringePlugin"])) .ToImmutableDictionary(b => b.CatalogEntry.Version); var version = reference.Range.FindBestMatch(items.Values.Select(b => b.CatalogEntry.Version)); @@ -56,8 +56,11 @@ public class PackageResolver(NuGetFramework runtimeFramework, ImmutableArray g.TargetFramework); if (nearestGroup is null) diff --git a/CringePlugins/Ui/PluginListComponent.cs b/CringePlugins/Ui/PluginListComponent.cs new file mode 100644 index 0000000..6cc427f --- /dev/null +++ b/CringePlugins/Ui/PluginListComponent.cs @@ -0,0 +1,310 @@ +using System.Collections.Immutable; +using System.Text.Json; +using CringePlugins.Abstractions; +using CringePlugins.Config; +using CringePlugins.Resolver; +using ImGuiNET; +using NLog; +using NuGet; +using NuGet.Models; +using NuGet.Versioning; +using Sandbox.Graphics.GUI; +using SpaceEngineers.Game.GUI; +using static ImGuiNET.ImGui; + +namespace CringePlugins.Ui; + +public class PluginListComponent : IRenderComponent +{ + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + private ImmutableDictionary _packages; + private ImmutableDictionary? _searchResults; + private string _searchQuery = ""; + private Task? _searchTask; + + private bool _changed; + private bool _open = true; + private readonly PackagesConfig _packagesConfig; + private readonly PackageSourceMapping _sources; + private readonly string _configPath; + private (SearchResultEntry entry, NuGetClient client)? _selected; + + public PluginListComponent(PackagesConfig packagesConfig, PackageSourceMapping sources, string configPath) + { + _packagesConfig = packagesConfig; + _sources = sources; + _configPath = configPath; + _packages = packagesConfig.Packages.ToImmutableDictionary(b => b.Id, b => b.Range, StringComparer.OrdinalIgnoreCase); + + MyGuiSandbox.GuiControlCreated += GuiControlCreated; + } + + private void GuiControlCreated(object obj) + { + if (obj is MyGuiScreenMainMenu) + _open = true; + } + + public void OnFrame() + { + if (!_open) return; + + SetNextWindowSize(new(700, 500), ImGuiCond.FirstUseEver); + + if (!Begin("Plugin List", ref _open)) + { + End(); + return; + } + + if (_changed) + TextDisabled("Changes would be applied on the next restart"); + + if (BeginTabBar("Main")) + { + // TODO support for opening plugin loader plugin config (reflection call to a specific method) + if (BeginTabItem("Installed Plugins")) + { + if (BeginTable("InstalledTable", 2, ImGuiTableFlags.ScrollY)) + { + TableSetupColumn("Id"); + TableSetupColumn("Version"); + TableHeadersRow(); + + foreach (var (id, versionRange) in _packages) + { + TableNextRow(); + + TableNextColumn(); + Text(id); + TableNextColumn(); + Text(versionRange.MinVersion?.ToString() ?? versionRange.ToString()); + } + + EndTable(); + } + + EndTabItem(); + } + + if (BeginTabItem("Available Plugins")) + { + AvailablePluginsTab(); + + EndTabItem(); + } + + EndTabBar(); + } + + End(); + } + + // TODO sources editor + // TODO combobox with active sources (to limit search results to specific list of sources) + private unsafe void AvailablePluginsTab() + { + if (_searchResults is null && _searchTask is null) + { + _searchTask = RefreshAsync(); + } + + switch (_searchTask) + { + case { IsCompleted: false }: + TextDisabled("Loading..."); + return; + case { IsCompletedSuccessfully: false }: + { + TextDisabled("Failed to load plugins list"); + if (_searchTask.Exception is null) return; + + foreach (var exception in _searchTask.Exception.InnerExceptions) + { + TextWrapped($"{exception.GetType()}: {exception.Message}"); + } + + return; + } + } + + var searchResults = _searchResults ?? ImmutableDictionary.Empty; + + InputText("", ref _searchQuery, 256); + + SameLine(); + + if (Button("Search")) + { + _searchTask = RefreshAsync(); + return; + } + + if (searchResults.IsEmpty || searchResults.Values.All(b => b.Entries.IsEmpty)) + { + TextDisabled("Nothing found"); + return; + } + + BeginChild("List", new(400, 0), ImGuiChildFlags.Border | ImGuiChildFlags.ResizeX); + { + if (BeginTable("AvailableTable", 3, ImGuiTableFlags.ScrollY | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingStretchProp)) + { + TableSetupColumn("Id", ImGuiTableColumnFlags.None, .5f); + TableSetupColumn("Version", ImGuiTableColumnFlags.None, .3f); + TableSetupColumn("Installed", ImGuiTableColumnFlags.None, .2f); + TableHeadersRow(); + + foreach (var (client, result) in searchResults) + { + foreach (var package in result.Entries.Take(100)) + { + TableNextRow(); + + TableNextColumn(); + + var selected = _selected?.entry.Id.Equals(package.Id, StringComparison.OrdinalIgnoreCase) == true; + + if (Selectable(package.Title ?? package.Id, ref selected, ImGuiSelectableFlags.SpanAllColumns)) + { + _selected = selected ? (package, client) : null; + } + + if (!string.IsNullOrEmpty(package.Summary) && IsItemHovered(ImGuiHoveredFlags.ForTooltip)) + { + SetTooltip(package.Summary); + } + + TableNextColumn(); + Text(package.Version.ToString()); + + TableNextColumn(); + + var installed = _packages.ContainsKey(package.Id); + BeginDisabled(); + if (Checkbox("", ref installed)) + { + _packages = installed + ? _packages.Add(package.Id, new(package.Version)) + : _packages.Remove(package.Id); + + Save(); + } + EndDisabled(); + } + } + + EndTable(); + } + + EndChild(); + } + + SameLine(); + + BeginGroup(); + + BeginChild("Package View", new(0, -GetFrameHeightWithSpacing())); // Leave room for 1 line below us + + if (_selected is not null) + { + var selected = _selected.Value.entry; + + Text(selected.Title ?? selected.Id); + SameLine(); + TextColored(*GetStyleColorVec4(ImGuiCol.TextLink), selected.Version.ToString()); + Separator(); + if (BeginTabBar("##PackageViewTabs")) + { + if (BeginTabItem("Description")) + { + TextWrapped(selected.Description ?? "Nothing."); + EndTabItem(); + } + + if (BeginTabItem("Details")) + { + Text("Pulled from"); + SameLine(); + var url = _selected.Value.client.ToString(); + TextLinkOpenURL(url, url); + + if (selected.Authors is not null) + { + Text("Authors:"); + SameLine(); + TextWrapped(selected.Authors.Author); + } + + if (selected.TotalDownloads.HasValue) + { + Text("Total downloads:"); + SameLine(); + TextColored(*GetStyleColorVec4(ImGuiCol.TextLink), selected.TotalDownloads.ToString()); + } + if (!string.IsNullOrEmpty(selected.ProjectUrl)) + TextLinkOpenURL("Project URL", selected.ProjectUrl); + + EndTabItem(); + } + + EndTabBar(); + } + } + + EndChild(); + + if (_selected is not null) + { + var selected = _selected.Value.entry; + + var installed = _packages.ContainsKey(selected.Id); + if (Button(installed ? "Uninstall" : "Install")) + { + _packages = installed + ? _packages.Remove(selected.Id) + : _packages.Add(selected.Id, new(selected.Version)); + + Save(); + } + } + + EndGroup(); + } + + private async Task RefreshAsync() + { + _searchResults = null; + + var builder = ImmutableDictionary.CreateBuilder(); + + await foreach (var source in _sources) + { + try + { + var result = await source.SearchPackagesAsync(_searchQuery, take: 1000, packageType: "CringePlugin"); + + builder.Add(source, result); + } + catch (Exception e) + { + Log.Error(e, "Failed to get packages from source {Source}", source); + } + } + + _searchResults = builder.ToImmutable(); + } + + private void Save() + { + _changed = true; + + using var stream = File.Create(_configPath); + + JsonSerializer.Serialize(stream, _packagesConfig with + { + Packages = [.._packages.Select(b => new PackageReference(b.Key, b.Value))] + }, NuGetClient.SerializerOptions); + } +} \ No newline at end of file diff --git a/NuGet.config b/NuGet.config index 3be5aa7..64fa657 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/NuGet/Converters/PackageAuthorsJsonConverter.cs b/NuGet/Converters/PackageAuthorsJsonConverter.cs new file mode 100644 index 0000000..a186234 --- /dev/null +++ b/NuGet/Converters/PackageAuthorsJsonConverter.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; +using NuGet.Models; + +namespace NuGet.Converters; + +public class PackageAuthorsJsonConverter : JsonConverter +{ + public override PackageAuthors? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + { + var author = reader.GetString()!; + return new PackageAuthors(author, [author]); + } + case JsonTokenType.StartArray: + { + var builder = ImmutableArray.CreateBuilder(); + + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + builder.Add(reader.GetString()!); + } + + return new PackageAuthors(string.Join(", ", builder), builder.ToImmutable()); + } + case JsonTokenType.Null: + return null; + default: + throw new JsonException("String or array of strings expected"); + } + } + + public override void Write(Utf8JsonWriter writer, PackageAuthors value, JsonSerializerOptions options) + { + if (value.Authors.Length == 1) + { + writer.WriteStringValue(value.Author); + return; + } + + writer.WriteStartArray(); + + foreach (var author in value.Authors) + { + writer.WriteStringValue(author); + } + + writer.WriteEndArray(); + } +} \ No newline at end of file diff --git a/NuGet/Converters/StringOrStringArrayConverter.cs b/NuGet/Converters/StringOrStringArrayConverter.cs new file mode 100644 index 0000000..18070c1 --- /dev/null +++ b/NuGet/Converters/StringOrStringArrayConverter.cs @@ -0,0 +1,48 @@ +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NuGet.Converters; + +public class StringOrStringArrayConverter : JsonConverter> +{ + public override ImmutableArray Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + return [reader.GetString()!]; + case JsonTokenType.StartArray: + { + var builder = ImmutableArray.CreateBuilder(); + + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + builder.Add(reader.GetString()!); + } + + return builder.ToImmutable(); + } + default: + throw new JsonException("String or array of strings expected"); + } + } + + public override void Write(Utf8JsonWriter writer, ImmutableArray value, JsonSerializerOptions options) + { + if (value.Length == 1) + { + writer.WriteStringValue(value[0]); + return; + } + + writer.WriteStartArray(); + + foreach (var author in value) + { + writer.WriteStringValue(author); + } + + writer.WriteEndArray(); + } +} \ No newline at end of file diff --git a/NuGet/Deps/DependenciesManifest.cs b/NuGet/Deps/DependenciesManifest.cs index b726c4b..e45eb97 100644 --- a/NuGet/Deps/DependenciesManifest.cs +++ b/NuGet/Deps/DependenciesManifest.cs @@ -88,10 +88,10 @@ public class DependencyManifestBuilder(DirectoryInfo cacheDirectory, PackageSour private async Task MapCatalogEntryAsync(CatalogEntry catalogEntry, NuGetFramework targetFramework, ImmutableDictionary.Builder targets) { - if (targets.ContainsKey(new(catalogEntry.Id, catalogEntry.Version))) + if (targets.ContainsKey(new(catalogEntry.Id, catalogEntry.Version)) || !catalogEntry.DependencyGroups.HasValue) return; - var nearest = NuGetFrameworkUtility.GetNearest(catalogEntry.DependencyGroups, targetFramework, + var nearest = NuGetFrameworkUtility.GetNearest(catalogEntry.DependencyGroups.Value, targetFramework, group => group.TargetFramework); if (nearest is null) @@ -103,9 +103,18 @@ public class DependencyManifestBuilder(DirectoryInfo cacheDirectory, PackageSour foreach (var dependency in nearest.Dependencies ?? []) { var client = await packageSources.GetClientAsync(dependency.Id); - var (url, entry) = await client.GetPackageRegistrationAsync(dependency.Id, versionResolver(dependency)!); + var registrationRoot = await client.GetPackageRegistrationRootAsync(dependency.Id); - entry ??= await client.GetPackageCatalogEntryAsync(url); + var version = versionResolver(dependency)!; + var entry = registrationRoot.Items.SelectMany(b => b.Items ?? []).FirstOrDefault(b => b.CatalogEntry.Version == version)?.CatalogEntry; + + if (entry is null) + { + var (url, sleetEntry) = await client.GetPackageRegistrationAsync(dependency.Id, versionResolver(dependency)!); + + entry = sleetEntry; + entry ??= await client.GetPackageCatalogEntryAsync(url); + } await MapCatalogEntryAsync(entry, targetFramework, targets); } diff --git a/NuGet/Models/CatalogEntry.cs b/NuGet/Models/CatalogEntry.cs index b36d143..5c10e6e 100644 --- a/NuGet/Models/CatalogEntry.cs +++ b/NuGet/Models/CatalogEntry.cs @@ -3,7 +3,7 @@ using NuGet.Versioning; namespace NuGet.Models; -public record CatalogEntry(string Id, NuGetVersion Version, ImmutableArray DependencyGroups, ImmutableArray? PackageTypes, +public record CatalogEntry(string Id, NuGetVersion Version, ImmutableArray? DependencyGroups, ImmutableArray? PackageTypes, ImmutableArray? PackageEntries); public record CatalogPackageEntry(string Name, string FullName, long CompressedLength, long Length); \ No newline at end of file diff --git a/NuGet/Models/PackageAuthors.cs b/NuGet/Models/PackageAuthors.cs new file mode 100644 index 0000000..08f9332 --- /dev/null +++ b/NuGet/Models/PackageAuthors.cs @@ -0,0 +1,8 @@ +using System.Collections.Immutable; +using System.Text.Json.Serialization; +using NuGet.Converters; + +namespace NuGet.Models; + +[JsonConverter(typeof(PackageAuthorsJsonConverter))] +public record PackageAuthors(string Author, ImmutableArray Authors); \ No newline at end of file diff --git a/NuGet/Models/PackageType.cs b/NuGet/Models/PackageType.cs new file mode 100644 index 0000000..b1d767f --- /dev/null +++ b/NuGet/Models/PackageType.cs @@ -0,0 +1,3 @@ +namespace NuGet.Models; + +public record PackageType(string Name); \ No newline at end of file diff --git a/NuGet/Models/RegistrationPage.cs b/NuGet/Models/RegistrationPage.cs index 4626929..5204f0e 100644 --- a/NuGet/Models/RegistrationPage.cs +++ b/NuGet/Models/RegistrationPage.cs @@ -2,4 +2,4 @@ namespace NuGet.Models; -public record RegistrationPage(int Count, NuGetVersion Lower, NuGetVersion Upper, RegistrationEntry[] Items); \ No newline at end of file +public record RegistrationPage(int Count, NuGetVersion Lower, NuGetVersion Upper, RegistrationEntry[]? Items); \ No newline at end of file diff --git a/NuGet/Models/SearchResult.cs b/NuGet/Models/SearchResult.cs new file mode 100644 index 0000000..0d27b0d --- /dev/null +++ b/NuGet/Models/SearchResult.cs @@ -0,0 +1,33 @@ +using System.Collections.Immutable; +using System.Text.Json.Serialization; +using NuGet.Converters; +using NuGet.Versioning; + +namespace NuGet.Models; + +public record SearchResult(int TotalHits, [property: JsonPropertyName("data")] ImmutableArray Entries); + +public record SearchResultEntry( + string Id, + NuGetVersion Version, + string? Description, + ImmutableArray Versions, + PackageAuthors? Authors, + string? IconUrl, + string? LicenseUrl, + [property: JsonConverter(typeof(StringOrStringArrayConverter))] + ImmutableArray? Owners, + string? ProjectUrl, + Uri Registration, + string? Summary, + [property: JsonConverter(typeof(StringOrStringArrayConverter))] + ImmutableArray? Tags, + string? Title, + int? TotalDownloads, + ImmutableArray PackageTypes, + bool Verified = false); + +public record SearchResultPackageVersion( + NuGetVersion Version, + int Downloads, + [property: JsonPropertyName("@id")] Uri Registration); diff --git a/NuGet/NuGetClient.cs b/NuGet/NuGetClient.cs index 7d4fc80..7d9c1aa 100644 --- a/NuGet/NuGetClient.cs +++ b/NuGet/NuGetClient.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Http.Json; using System.Text.Json; +using System.Web; using NuGet.Converters; using NuGet.Models; using NuGet.Versioning; @@ -9,9 +10,11 @@ namespace NuGet; public class NuGetClient { + private readonly Uri _index; private readonly HttpClient _client; private readonly Uri _packageBaseAddress; private readonly Uri _registration; + private readonly Uri _search; public static JsonSerializerOptions SerializerOptions { get; } = new(JsonSerializerDefaults.Web) { @@ -24,11 +27,13 @@ public class NuGetClient WriteIndented = true }; - private NuGetClient(HttpClient client, Uri packageBaseAddress, Uri registration) + private NuGetClient(Uri index, HttpClient client, Uri packageBaseAddress, Uri registration, Uri search) { + _index = index; _client = client; _packageBaseAddress = packageBaseAddress; _registration = registration; + _search = search; } public Task GetPackageContentStreamAsync(string id, NuGetVersion version) @@ -61,6 +66,37 @@ public class NuGetClient return _client.GetFromJsonAsync(url, SerializerOptions)!; } + public Task SearchPackagesAsync(string? query = null, int? skip = null, int? take = null, + bool? includePrerelease = null, NuGetVersion? minVersion = null, string? packageType = null) + { + var queryParameters = HttpUtility.ParseQueryString(string.Empty); + + if (!string.IsNullOrEmpty(query)) + queryParameters.Add("q", query); + + if (skip.HasValue) + queryParameters.Add("skip", skip.Value.ToString()); + + if (take.HasValue) + queryParameters.Add("take", take.Value.ToString()); + + if (includePrerelease.HasValue) + queryParameters.Add("prerelease", includePrerelease.Value.ToString()); + + if (minVersion is not null) + queryParameters.Add("semVerLevel", minVersion.ToString()); + + if (!string.IsNullOrEmpty(packageType)) + queryParameters.Add("packageType", packageType); + + var builder = new UriBuilder(_search) + { + Query = queryParameters.ToString() + }; + + return _client.GetFromJsonAsync(builder.Uri, SerializerOptions)!; + } + public static async Task CreateFromIndexUrlAsync(string indexUrl) { var client = new HttpClient(new HttpClientHandler @@ -71,13 +107,20 @@ public class NuGetClient var index = await client.GetFromJsonAsync(indexUrl, SerializerOptions); var (packageBaseAddress, _, _) = index!.Resources.First(b => b.Type.Id == "PackageBaseAddress"); - var (registration, _, _) = index!.Resources.First(b => b.Type.Id == "RegistrationsBaseUrl"); + var (registration, _, _) = index.Resources.First(b => b.Type.Id == "RegistrationsBaseUrl"); + var (search, _, _) = index.Resources.First(b => b.Type.Id == "SearchQueryService"); if (!packageBaseAddress.EndsWith('/')) packageBaseAddress += '/'; if (!registration.EndsWith('/')) registration += '/'; - - return new NuGetClient(client, new Uri(packageBaseAddress), new Uri(registration)); + + return new NuGetClient(new Uri(indexUrl), client, new Uri(packageBaseAddress), new Uri(registration), new Uri(search)); } + + public override string ToString() => _index.ToString(); + + public override bool Equals(object? obj) => obj is NuGetClient { _index: { } index } && index == _index; + + public override int GetHashCode() => _index.GetHashCode(); } \ No newline at end of file diff --git a/NuGet/PackageSourceMapping.cs b/NuGet/PackageSourceMapping.cs index 4d22b8f..b0ab0de 100644 --- a/NuGet/PackageSourceMapping.cs +++ b/NuGet/PackageSourceMapping.cs @@ -1,4 +1,7 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; namespace NuGet; @@ -11,7 +14,15 @@ public class PackageSourceMapping(ImmutableArray sources) ]; public Task GetClientAsync(string packageId) => - _clients.FirstOrDefault(b => packageId.StartsWith(b.pattern)).client; + _clients.FirstOrDefault(b => Regex.IsMatch(packageId, b.pattern)).client; + + public ConfiguredCancelableAsyncEnumerable.Enumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + return _clients.ToAsyncEnumerable() + .SelectAwait(b => new ValueTask(b.client)) + .WithCancellation(cancellationToken) + .GetAsyncEnumerator(); + } } -public record PackageSource(string Pattern, string Url); \ No newline at end of file +public record PackageSource([StringSyntax("Regex")] string Pattern, [StringSyntax("Uri")] string Url); \ No newline at end of file