diff --git a/Directory.Packages.props b/Directory.Packages.props index 596512f..5b6b6e9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,6 +27,7 @@ + diff --git a/src/SharpIDE.Application/Features/Nuget/NugetPackageIconCacheService.cs b/src/SharpIDE.Application/Features/Nuget/NugetPackageIconCacheService.cs new file mode 100644 index 0000000..484f299 --- /dev/null +++ b/src/SharpIDE.Application/Features/Nuget/NugetPackageIconCacheService.cs @@ -0,0 +1,52 @@ +namespace SharpIDE.Application.Features.Nuget; + +public enum NugetPackageIconFormat +{ + Png, + Jpg +} +public class NugetPackageIconCacheService(IHttpClientFactory httpClientFactory) +{ + private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; + + public async Task<(byte[]? bytes, NugetPackageIconFormat?)> GetNugetPackageIcon(string packageId, Uri? iconUrl) + { + var appdataFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var cacheFolder = Path.Combine(appdataFolderPath, "SharpIDE", "NugetPackageIconCache"); + Directory.CreateDirectory(cacheFolder); + var packageIconFilePath = Path.Combine(cacheFolder, $"{packageId}.bin"); + if (File.Exists(packageIconFilePath)) + { + var bytes = await File.ReadAllBytesAsync(packageIconFilePath); + return (bytes, GetImageFormat(bytes)); + } + else if (iconUrl is null) + { + return (null, null); + } + else + { + var httpClient = _httpClientFactory.CreateClient(); + var iconBytes = await httpClient.GetByteArrayAsync(iconUrl); + await File.WriteAllBytesAsync(packageIconFilePath, iconBytes); + return (iconBytes, GetImageFormat(iconBytes)); + } + } + + private static NugetPackageIconFormat? GetImageFormat(byte[] imageBytes) + { + // PNG files start with 89 50 4E 47 0D 0A 1A 0A + if (imageBytes is [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, ..]) + { + return NugetPackageIconFormat.Png; + } + + // JPEG files start with FF D8 and end with FF D9 + if (imageBytes is [0xFF, 0xD8, .., 0xFF, 0xD9]) + { + return NugetPackageIconFormat.Jpg; + } + + return null; + } +} diff --git a/src/SharpIDE.Application/SharpIDE.Application.csproj b/src/SharpIDE.Application/SharpIDE.Application.csproj index 4324197..93c437e 100644 --- a/src/SharpIDE.Application/SharpIDE.Application.csproj +++ b/src/SharpIDE.Application/SharpIDE.Application.csproj @@ -27,6 +27,7 @@ + diff --git a/src/SharpIDE.Godot/DiAutoload.cs b/src/SharpIDE.Godot/DiAutoload.cs index dd97a48..ef7285b 100644 --- a/src/SharpIDE.Godot/DiAutoload.cs +++ b/src/SharpIDE.Godot/DiAutoload.cs @@ -38,12 +38,15 @@ public partial class DiAutoload : Node services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + + services.AddHttpClient(); services.AddLogging(builder => { builder.AddConsole(); diff --git a/src/SharpIDE.Godot/Features/Nuget/PackageEntry.cs b/src/SharpIDE.Godot/Features/Nuget/PackageEntry.cs index 992b18c..1e4985f 100644 --- a/src/SharpIDE.Godot/Features/Nuget/PackageEntry.cs +++ b/src/SharpIDE.Godot/Features/Nuget/PackageEntry.cs @@ -17,6 +17,8 @@ public partial class PackageEntry : MarginContainer private static readonly Color Source_4_Color = new Color("966a00"); private static readonly Color Source_5_Color = new Color("efaeae"); + [Inject] private readonly NugetPackageIconCacheService _nugetPackageIconCacheService = null!; + public IdePackageResult PackageResult { get; set; } = null!; public override void _Ready() { @@ -35,26 +37,24 @@ public partial class PackageEntry : MarginContainer _currentVersionLabel.Text = string.Empty; //_latestVersionLabel.Text = $"Latest: {PackageResult.PackageSearchMetadata.vers.LatestVersion}"; _sourceNamesContainer.QueueFreeChildren(); - - var iconUrl = PackageResult.PackageSearchMetadata.IconUrl; - if (iconUrl != null) + + _ = Task.GodotRun(async () => { - var httpRequest = new HttpRequest(); // Godot's abstraction - AddChild(httpRequest); - httpRequest.RequestCompleted += (result, responseCode, headers, body) => + var (iconBytes, iconFormat) = await _nugetPackageIconCacheService.GetNugetPackageIcon(PackageResult.PackageSearchMetadata.Identity.Id, PackageResult.PackageSearchMetadata.IconUrl); + var image = new Image(); + var error = iconFormat switch { - if (responseCode is 200) - { - var image = new Image(); - image.LoadPngFromBuffer(body); - image.Resize(32, 32, Image.Interpolation.Lanczos); - var loadedImageTexture = ImageTexture.CreateFromImage(image); - _packageIconTextureRect.Texture = loadedImageTexture; - } - httpRequest.QueueFree(); + NugetPackageIconFormat.Png => image.LoadPngFromBuffer(iconBytes), + NugetPackageIconFormat.Jpg => image.LoadJpgFromBuffer(iconBytes), + _ => Error.FileUnrecognized }; - httpRequest.Request(iconUrl.ToString()); - } + if (error is Error.Ok) + { + image.Resize(32, 32, Image.Interpolation.Lanczos); // Probably should cache resized images instead + var loadedImageTexture = ImageTexture.CreateFromImage(image); + await this.InvokeAsync(() => _packageIconTextureRect.Texture = loadedImageTexture); + } + }); foreach (var source in PackageResult.PackageSources) { diff --git a/src/SharpIDE.Godot/SharpIDE.Godot.csproj b/src/SharpIDE.Godot/SharpIDE.Godot.csproj index 2da594c..cf93d7b 100644 --- a/src/SharpIDE.Godot/SharpIDE.Godot.csproj +++ b/src/SharpIDE.Godot/SharpIDE.Godot.csproj @@ -10,6 +10,7 @@ +