From 478db3cada65a18133d47a674717415a8b11cc55 Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 16 Dec 2024 15:05:25 +0100 Subject: [PATCH 1/4] Remove IImageCache usage from GuidedInstaller --- .../GuidedInstallerUi.cs | 2 +- .../Step/GuidedInstallerStepViewModel.cs | 49 ++++++++++--- src/NexusMods.App.UI/ImagePipelines.cs | 73 +++++++++++++++++-- 3 files changed, 109 insertions(+), 15 deletions(-) diff --git a/src/Games/NexusMods.Games.FOMOD.UI/GuidedInstallerUi.cs b/src/Games/NexusMods.Games.FOMOD.UI/GuidedInstallerUi.cs index 4699d37c6b..786704636c 100644 --- a/src/Games/NexusMods.Games.FOMOD.UI/GuidedInstallerUi.cs +++ b/src/Games/NexusMods.Games.FOMOD.UI/GuidedInstallerUi.cs @@ -120,7 +120,7 @@ private static void SetupStep( Percent progress) { var viewModel = window.ViewModel!; - viewModel.ActiveStepViewModel ??= currentScope.ServiceProvider.GetRequiredService(); + viewModel.ActiveStepViewModel ??= new GuidedInstallerStepViewModel(currentScope.ServiceProvider); var activeStepViewModel = viewModel.ActiveStepViewModel; activeStepViewModel.ModName = viewModel.WindowName; diff --git a/src/Games/NexusMods.Games.FOMOD.UI/Step/GuidedInstallerStepViewModel.cs b/src/Games/NexusMods.Games.FOMOD.UI/Step/GuidedInstallerStepViewModel.cs index 15f27317c3..fdabe8dfe8 100644 --- a/src/Games/NexusMods.Games.FOMOD.UI/Step/GuidedInstallerStepViewModel.cs +++ b/src/Games/NexusMods.Games.FOMOD.UI/Step/GuidedInstallerStepViewModel.cs @@ -29,6 +29,7 @@ public class GuidedInstallerStepViewModel : AViewModel _highlightedOptionImage; public IImage? HighlightedOptionImage => _highlightedOptionImage.Value; @@ -41,8 +42,11 @@ public class GuidedInstallerStepViewModel : AViewModel optionVM.Option.Image) .WhereNotNull() - .Select(optionImage => + .OffUi() + .SelectMany(async optionImage => + { + try + { + if (optionImage.TryPickT0(out var uri, out var imageStoredFile)) + { + return await remoteImagePipeline.LoadResourceAsync(uri, CancellationToken.None); + } + else + { + return await fileImagePipeline.LoadResourceAsync(imageStoredFile.FileHash, CancellationToken.None); + } + } + catch (Exception e) + { + return null; + } + }) + .Select(static resource => resource?.Data) + .Do(lifetime => { - return optionImage.Match( - f0: uri => new ImageIdentifier(uri), - f1: imageStoredFile => new ImageIdentifier(imageStoredFile.FileHash) - ); + _imageDisposable?.Dispose(); + _imageDisposable = lifetime; }) - .OffUi() - .SelectMany(imageCache.GetImage) + .Select(static lifetime => lifetime?.Value) .WhereNotNull() .OnUI() .ToProperty(this, vm => vm.HighlightedOptionImage); var goToNextCommand = ReactiveCommand.Create(() => { + _imageDisposable?.Dispose(); + _imageDisposable = null; + // NOTE(erri120): On the last step, we don't set the result but instead show a "installation complete"-screen. if (InstallationStep!.HasNextStep || ShowInstallationCompleteScreen) { @@ -85,6 +109,9 @@ public GuidedInstallerStepViewModel(IImageCache imageCache) var goToPrevCommand = ReactiveCommand.Create(() => { + _imageDisposable?.Dispose(); + _imageDisposable = null; + if (ShowInstallationCompleteScreen) { ShowInstallationCompleteScreen = false; @@ -180,7 +207,11 @@ public GuidedInstallerStepViewModel(IImageCache imageCache) .BindToVM(this, vm => vm.FooterStepperViewModel.Progress) .DisposeWith(disposables); - Disposable.Create(() => _highlightedOptionImage.Dispose()).DisposeWith(disposables); + Disposable.Create(() => + { + _imageDisposable?.Dispose(); + _highlightedOptionImage.Dispose(); + }).DisposeWith(disposables); }); } diff --git a/src/NexusMods.App.UI/ImagePipelines.cs b/src/NexusMods.App.UI/ImagePipelines.cs index 3989f9f550..e5a87fc68e 100644 --- a/src/NexusMods.App.UI/ImagePipelines.cs +++ b/src/NexusMods.App.UI/ImagePipelines.cs @@ -1,8 +1,12 @@ using Avalonia.Media.Imaging; using Avalonia.Platform; +using BitFaster.Caching; +using BitFaster.Caching.Lru; using Microsoft.Extensions.DependencyInjection; +using NexusMods.Abstractions.IO; using NexusMods.Abstractions.NexusModsLibrary.Models; using NexusMods.Abstractions.Resources; +using NexusMods.Abstractions.Resources.Caching; using NexusMods.Abstractions.Resources.DB; using NexusMods.Abstractions.Resources.IO; using NexusMods.Abstractions.Resources.Resilience; @@ -13,12 +17,14 @@ namespace NexusMods.App.UI; -internal static class ImagePipelines +public static class ImagePipelines { private const byte ImagePartitionId = 10; private const string CollectionTileImagePipelineKey = nameof(CollectionTileImagePipelineKey); private const string CollectionBackgroundImagePipelineKey = nameof(CollectionBackgroundImagePipelineKey); private const string UserAvatarPipelineKey = nameof(UserAvatarPipelineKey); + private const string GuidedInstallerRemoteImagePipelineKey = nameof(GuidedInstallerRemoteImagePipelineKey); + private const string GuidedInstallerFileImagePipelineKey = nameof(GuidedInstallerFileImagePipelineKey); private static readonly Bitmap CollectionTileFallback = new(AssetLoader.Open(new Uri("avares://NexusMods.App.UI/Assets/collection-tile-fallback.png"))); private static readonly Bitmap CollectionBackgroundFallback = new(AssetLoader.Open(new Uri("avares://NexusMods.App.UI/Assets/black-box.png"))); @@ -39,20 +45,35 @@ public static IServiceCollection AddImagePipelines(this IServiceCollection servi .AddKeyedSingleton>( serviceKey: UserAvatarPipelineKey, implementationFactory: static (serviceProvider, _) => CreateUserAvatarPipeline( + httpClient: serviceProvider.GetRequiredService(), connection: serviceProvider.GetRequiredService() ) ) .AddKeyedSingleton>( serviceKey: CollectionTileImagePipelineKey, implementationFactory: static (serviceProvider, _) => CreateCollectionTileImagePipeline( + httpClient: serviceProvider.GetRequiredService(), connection: serviceProvider.GetRequiredService() ) ) .AddKeyedSingleton>( serviceKey: CollectionBackgroundImagePipelineKey, implementationFactory: static (serviceProvider, _) => CreateCollectionBackgroundImagePipeline( + httpClient: serviceProvider.GetRequiredService(), connection: serviceProvider.GetRequiredService() ) + ) + .AddKeyedSingleton>>( + serviceKey: GuidedInstallerRemoteImagePipelineKey, + implementationFactory: static (serviceProvider, _) => CreateGuidedInstallerRemoteImagePipeline( + httpClient: serviceProvider.GetRequiredService() + ) + ) + .AddKeyedSingleton>>( + serviceKey: GuidedInstallerFileImagePipelineKey, + implementationFactory: static (serviceProvider, _) => CreateGuidedInstallerFileImagePipeline( + fileStore: serviceProvider.GetRequiredService() + ) ); } @@ -71,9 +92,21 @@ public static IResourceLoader GetCollectionBackgroundImagePipe return serviceProvider.GetRequiredKeyedService>(serviceKey: CollectionBackgroundImagePipelineKey); } - private static IResourceLoader CreateUserAvatarPipeline(IConnection connection) + public static IResourceLoader> GetGuidedInstallerRemoteImagePipeline(IServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredKeyedService>>(serviceKey: GuidedInstallerRemoteImagePipelineKey); + } + + public static IResourceLoader> GetGuidedInstallerFileImagePipeline(IServiceProvider serviceProvider) { - var pipeline = new HttpLoader(new HttpClient()) + return serviceProvider.GetRequiredKeyedService>>(serviceKey: GuidedInstallerFileImagePipelineKey); + } + + private static IResourceLoader CreateUserAvatarPipeline( + HttpClient httpClient, + IConnection connection) + { + var pipeline = new HttpLoader(httpClient) .ChangeIdentifier, Uri, byte[]>(static tuple => tuple.Item2) .PersistInDb( connection: connection, @@ -93,9 +126,10 @@ private static IResourceLoader CreateUserAvatarPipeline(IConne } private static IResourceLoader CreateCollectionTileImagePipeline( + HttpClient httpClient, IConnection connection) { - var pipeline = new HttpLoader(new HttpClient()) + var pipeline = new HttpLoader(httpClient) .ChangeIdentifier, Uri, byte[]>(static tuple => tuple.Item2) .PersistInDb( connection: connection, @@ -115,9 +149,10 @@ private static IResourceLoader CreateCollectionTileImagePipeli } private static IResourceLoader CreateCollectionBackgroundImagePipeline( + HttpClient httpClient, IConnection connection) { - var pipeline = new HttpLoader(new HttpClient()) + var pipeline = new HttpLoader(httpClient) .ChangeIdentifier, Uri, byte[]>(static tuple => tuple.Item2) .PersistInDb( connection: connection, @@ -135,4 +170,32 @@ private static IResourceLoader CreateCollectionBackgroundImage return pipeline; } + + private static IResourceLoader> CreateGuidedInstallerRemoteImagePipeline(HttpClient httpClient) + { + var pipeline = new HttpLoader(httpClient) + .Decode(decoderType: DecoderType.Skia) + .ToAvaloniaBitmap() + .UseScopedCache( + keyGenerator: static uri => uri, + keyComparer: EqualityComparer.Default, + capacityPartition: new FavorWarmPartition(totalCapacity: 10) + ); + + return pipeline; + } + + private static IResourceLoader> CreateGuidedInstallerFileImagePipeline(IFileStore fileStore) + { + var pipeline = new FileStoreLoader(fileStore) + .Decode(decoderType: DecoderType.Skia) + .ToAvaloniaBitmap() + .UseScopedCache( + keyGenerator: static hash => hash, + keyComparer: EqualityComparer.Default, + capacityPartition: new FavorWarmPartition(totalCapacity: 10) + ); + + return pipeline; + } } From ab2452fb7c0951eba9d1969962ff3d7d525edc6c Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 16 Dec 2024 15:21:29 +0100 Subject: [PATCH 2/4] Use image pipeline in markdown renderer --- .../MarkdownRendererViewModel.cs | 31 +++++++++---------- src/NexusMods.App.UI/ImagePipelines.cs | 21 +++++++++++++ 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererViewModel.cs b/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererViewModel.cs index 3efcd5247e..22ffaa3077 100644 --- a/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererViewModel.cs +++ b/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererViewModel.cs @@ -1,15 +1,17 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Text; using Avalonia.Media; +using Avalonia.Media.Imaging; using JetBrains.Annotations; using Markdown.Avalonia.Plugins; using Markdown.Avalonia.Utils; using Microsoft.Extensions.Logging; +using NexusMods.Abstractions.Resources; using NexusMods.Abstractions.UI; using NexusMods.App.UI.Extensions; using NexusMods.CrossPlatform.Process; -using NexusMods.Hashing.xxHash3; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -19,8 +21,8 @@ namespace NexusMods.App.UI.Controls.MarkdownRenderer; public class MarkdownRendererViewModel : AViewModel, IMarkdownRendererViewModel { private readonly ILogger _logger; - private readonly IImageCache _imageCache; private readonly HttpClient _httpClient; + private readonly IResourceLoader _remoteImagePipeline; [Reactive] public string Contents { get; set; } = string.Empty; [Reactive] public Uri? MarkdownUri { get; set; } @@ -31,14 +33,14 @@ public class MarkdownRendererViewModel : AViewModel, public ReactiveCommand OpenLinkCommand { get; } public MarkdownRendererViewModel( + IServiceProvider serviceProvider, ILogger logger, IOSInterop osInterop, - IImageCache imageCache, HttpClient httpClient) { _logger = logger; - _imageCache = imageCache; _httpClient = httpClient; + _remoteImagePipeline = ImagePipelines.GetMarkdownRendererRemoteImagePipeline(serviceProvider); PathResolver = new PathResolverImpl(this); ImageResolverPlugin = new ImageResolvePluginImpl(new ImageResolverImpl(this)); @@ -120,14 +122,11 @@ private async Task FetchMarkdown(Uri uri, CancellationToken cancellation return await FetchRemoteImage(uri, cancellationToken); } - private async Task FetchRemoteImage(Uri uri, CancellationToken cancellationToken = default) + private Task FetchRemoteImage(Uri uri, CancellationToken cancellationToken = default) { - var hash = await _imageCache.Prefetch(new ImageIdentifier(uri), cancellationToken); - var hashValue = hash.Value; - - var bytes = BitConverter.GetBytes(hashValue); + var bytes = Encoding.UTF8.GetBytes(uri.ToString()); var ms = new MemoryStream(bytes, writable: false); - return ms; + return Task.FromResult(ms); } private class ImageResolvePluginImpl : IMdAvPlugin @@ -191,14 +190,12 @@ public ImageResolverImpl(MarkdownRendererViewModel parent) public async Task Load(Stream stream) { - var bytes = GC.AllocateUninitializedArray(sizeof(ulong)); - stream.ReadExactly(bytes); - - var hashValue = BitConverter.ToUInt64(bytes); - var hash = Hash.FromULong(hashValue); + using var sr = new StreamReader(stream, Encoding.UTF8); + var url = await sr.ReadToEndAsync(); + var uri = new Uri(url, UriKind.Absolute); - var image = await _parent._imageCache.GetImage(new ImageIdentifier(hash), CancellationToken.None); - return image; + var resource = await _parent._remoteImagePipeline.LoadResourceAsync(uri, CancellationToken.None); + return resource.Data; } } } diff --git a/src/NexusMods.App.UI/ImagePipelines.cs b/src/NexusMods.App.UI/ImagePipelines.cs index e5a87fc68e..9884693bf5 100644 --- a/src/NexusMods.App.UI/ImagePipelines.cs +++ b/src/NexusMods.App.UI/ImagePipelines.cs @@ -25,6 +25,7 @@ public static class ImagePipelines private const string UserAvatarPipelineKey = nameof(UserAvatarPipelineKey); private const string GuidedInstallerRemoteImagePipelineKey = nameof(GuidedInstallerRemoteImagePipelineKey); private const string GuidedInstallerFileImagePipelineKey = nameof(GuidedInstallerFileImagePipelineKey); + private const string MarkdownRendererRemoteImagePipelineKey = nameof(MarkdownRendererRemoteImagePipelineKey); private static readonly Bitmap CollectionTileFallback = new(AssetLoader.Open(new Uri("avares://NexusMods.App.UI/Assets/collection-tile-fallback.png"))); private static readonly Bitmap CollectionBackgroundFallback = new(AssetLoader.Open(new Uri("avares://NexusMods.App.UI/Assets/black-box.png"))); @@ -74,6 +75,12 @@ public static IServiceCollection AddImagePipelines(this IServiceCollection servi implementationFactory: static (serviceProvider, _) => CreateGuidedInstallerFileImagePipeline( fileStore: serviceProvider.GetRequiredService() ) + ) + .AddKeyedSingleton>( + serviceKey: MarkdownRendererRemoteImagePipelineKey, + implementationFactory: static (serviceProvider, _) => CreateMarkdownRendererRemoteImagePipeline( + httpClient: serviceProvider.GetRequiredService() + ) ); } @@ -102,6 +109,11 @@ public static IResourceLoader> GetGuidedInstallerFileImag return serviceProvider.GetRequiredKeyedService>>(serviceKey: GuidedInstallerFileImagePipelineKey); } + public static IResourceLoader GetMarkdownRendererRemoteImagePipeline(IServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredKeyedService>(serviceKey: MarkdownRendererRemoteImagePipelineKey); + } + private static IResourceLoader CreateUserAvatarPipeline( HttpClient httpClient, IConnection connection) @@ -185,6 +197,15 @@ private static IResourceLoader> CreateGuidedInstallerRemot return pipeline; } + private static IResourceLoader CreateMarkdownRendererRemoteImagePipeline(HttpClient httpClient) + { + var pipeline = new HttpLoader(httpClient) + .Decode(decoderType: DecoderType.Skia) + .ToAvaloniaBitmap(); + + return pipeline; + } + private static IResourceLoader> CreateGuidedInstallerFileImagePipeline(IFileStore fileStore) { var pipeline = new FileStoreLoader(fileStore) From a235e0bd574dedf0f21794b13ae91c5ee224ebf1 Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 16 Dec 2024 15:21:48 +0100 Subject: [PATCH 3/4] Remove IImageCache --- src/NexusMods.App.UI/IImageCache.cs | 34 ----- src/NexusMods.App.UI/ImageCache.cs | 143 ------------------- src/NexusMods.App.UI/NexusMods.App.UI.csproj | 3 - src/NexusMods.App.UI/Services.cs | 1 - tests/NexusMods.UI.Tests/ImageCacheTests.cs | 80 ----------- 5 files changed, 261 deletions(-) delete mode 100644 src/NexusMods.App.UI/IImageCache.cs delete mode 100644 src/NexusMods.App.UI/ImageCache.cs delete mode 100644 tests/NexusMods.UI.Tests/ImageCacheTests.cs diff --git a/src/NexusMods.App.UI/IImageCache.cs b/src/NexusMods.App.UI/IImageCache.cs deleted file mode 100644 index 4bdfd24baf..0000000000 --- a/src/NexusMods.App.UI/IImageCache.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Avalonia.Media; -using JetBrains.Annotations; -using NexusMods.Hashing.xxHash3; -using OneOf; - -namespace NexusMods.App.UI; - -/// -/// Represents an image cache. -/// -[PublicAPI] -[Obsolete("To be replaced with resource pipelines")] -public interface IImageCache : IDisposable -{ - /// - /// Gets an image from cache or loads the image. - /// - Task GetImage(ImageIdentifier imageIdentifier, CancellationToken cancellationToken); - - /// - /// Prefetches the provided image. - /// - Task Prefetch(ImageIdentifier imageIdentifier, CancellationToken cancellationToken); -} - -public readonly struct ImageIdentifier -{ - public readonly OneOf Union; - - public ImageIdentifier(OneOf union) - { - Union = union; - } -} diff --git a/src/NexusMods.App.UI/ImageCache.cs b/src/NexusMods.App.UI/ImageCache.cs deleted file mode 100644 index db66babcfe..0000000000 --- a/src/NexusMods.App.UI/ImageCache.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System.Xml; -using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Svg.Skia; -using Microsoft.Extensions.Logging; -using NexusMods.Abstractions.IO; -using NexusMods.Hashing.xxHash3; -using Svg.Model; - -namespace NexusMods.App.UI; - -internal sealed class ImageCache : IImageCache -{ - private readonly ILogger _logger; - private readonly IFileStore _fileStore; - private readonly HttpClient _client; - - private readonly Dictionary _cache = new(); - - public ImageCache( - ILogger logger, - IFileStore fileStore, - HttpClient client) - { - _logger = logger; - _fileStore = fileStore; - _client = client; - } - - public async Task GetImage(ImageIdentifier imageIdentifier, CancellationToken cancellationToken) - { - var hash = await Prefetch(imageIdentifier, cancellationToken); - return hash == Hash.Zero ? null : _cache.GetValueOrDefault(hash); - } - - public async Task Prefetch( - ImageIdentifier imageIdentifier, - CancellationToken cancellationToken) - { - var hash = GetHash(imageIdentifier); - if (_cache.TryGetValue(hash, out _)) return hash; - - var image = await Load(imageIdentifier, cancellationToken); - if (image is null) return Hash.Zero; - - _cache.TryAdd(hash, image); - return hash; - } - - private static Hash GetHash(ImageIdentifier imageIdentifier) - { - return imageIdentifier.Union.Match( - f0: uri => uri.ToString().xxHash3AsUtf8(), - f1: hash => hash - ); - } - - private Task Load(ImageIdentifier imageIdentifier, CancellationToken cancellationToken) - { - return imageIdentifier.Union.Match( - f0: uri => LoadFromUri(uri, cancellationToken), - f1: hash => LoadFromHash(hash, cancellationToken) - ); - } - - private async Task LoadFromUri(Uri uri, CancellationToken cancellationToken) - { - try - { - _logger.LogDebug("Fetching image from {Uri}", uri); - var bytes = await _client.GetByteArrayAsync(uri, cancellationToken); - var stream = new MemoryStream(bytes); - return StreamToImage(stream); - } - catch (Exception e) - { - _logger.LogError(e, "Exception while loading image from {Uri}", uri); - return null; - } - } - - private async Task LoadFromHash( - Hash hash, - CancellationToken cancellationToken) - { - try - { - await using var stream = await _fileStore.GetFileStream(hash, cancellationToken); - return StreamToImage(stream); - } - catch (Exception e) - { - _logger.LogError(e, "Exception while loading image from file store with hash {Hash}", hash); - return null; - } - } - - private static IImage? StreamToImage(Stream stream) - { - return IsSvg(stream) ? FromSvg(stream) : new Bitmap(stream); - } - - private static SvgImage? FromSvg(Stream stream) - { - var source = SvgSource.LoadFromStream(stream); - var image = new SvgImage - { - Source = source, - }; - - return image; - } - - private static bool IsSvg(Stream stream) - { - try - { - var firstByte = stream.ReadByte(); - if (firstByte != ('<' & 0xFF)) return false; - - stream.Seek(0, SeekOrigin.Begin); - using var xmlReader = XmlReader.Create(stream); - return xmlReader.MoveToContent() == XmlNodeType.Element && "svg".Equals(xmlReader.Name, StringComparison.OrdinalIgnoreCase); - } - catch - { - return false; - } - finally - { - stream.Seek(0, SeekOrigin.Begin); - } - } - - public void Dispose() - { - foreach (var kv in _cache) - { - var value = kv.Value; - if (value is IDisposable disposable) disposable.Dispose(); - } - } -} diff --git a/src/NexusMods.App.UI/NexusMods.App.UI.csproj b/src/NexusMods.App.UI/NexusMods.App.UI.csproj index bc0bb86454..88be91e768 100644 --- a/src/NexusMods.App.UI/NexusMods.App.UI.csproj +++ b/src/NexusMods.App.UI/NexusMods.App.UI.csproj @@ -512,9 +512,6 @@ INewTabPageSectionViewModel.cs - - IImageCache.cs - ITextEditorPageViewModel.cs diff --git a/src/NexusMods.App.UI/Services.cs b/src/NexusMods.App.UI/Services.cs index 0e118eb980..e67159855b 100644 --- a/src/NexusMods.App.UI/Services.cs +++ b/src/NexusMods.App.UI/Services.cs @@ -89,7 +89,6 @@ public static IServiceCollection AddUI(this IServiceCollection c) // Services .AddSingleton() - .AddTransient() // View Models .AddTransient() diff --git a/tests/NexusMods.UI.Tests/ImageCacheTests.cs b/tests/NexusMods.UI.Tests/ImageCacheTests.cs deleted file mode 100644 index fc06839bbb..0000000000 --- a/tests/NexusMods.UI.Tests/ImageCacheTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using NexusMods.Abstractions.IO; -using NexusMods.Abstractions.IO.StreamFactories; -using NexusMods.App.UI; -using NexusMods.Hashing.xxHash3; -using NexusMods.Paths; -using NexusMods.Paths.Extensions; - -namespace NexusMods.UI.Tests; - -// NOTE(erri120): This inherits from AUiTest because the Avalonia -// Bitmap class requires Avalonia to be initialized beforehand. -public class ImageCacheTests : AUiTest -{ - private readonly IServiceProvider _serviceProvider; - - public ImageCacheTests(IServiceProvider serviceProvider) : base(serviceProvider) - { - _serviceProvider = serviceProvider; - } - - [Fact] - public async Task Test_LoadAndCache_RemoteImage() - { - const string url = "https://http.cat/418.jpg"; - var uri = new Uri(url); - - using var scope = _serviceProvider.CreateScope(); - using var imageCache = scope.ServiceProvider.GetRequiredService(); - - var image1 = await imageCache.GetImage(new ImageIdentifier(uri), cancellationToken: default); - image1.Should().NotBeNull(); - - var image2 = await imageCache.GetImage(new ImageIdentifier(uri), cancellationToken: default); - image2.Should().NotBeNull(); - - image1.Should().BeSameAs(image2); - } - - [Fact] - public async Task Test_LoadAndCache_ImageStoredFile() - { - var hash = await PrepareImage(); - - using var scope = _serviceProvider.CreateScope(); - using var imageCache = scope.ServiceProvider.GetRequiredService(); - - var image1 = await imageCache.GetImage(new ImageIdentifier(hash), cancellationToken: default); - image1.Should().NotBeNull(); - - var image2 = await imageCache.GetImage(new ImageIdentifier(hash), cancellationToken: default); - image2.Should().NotBeNull(); - - image1.Should().BeSameAs(image2); - } - - private async Task PrepareImage() - { - var archiveManager = _serviceProvider.GetRequiredService(); - - const string url = "https://http.cat/418.jpg"; - var httpClient = new HttpClient(); - var bytes = await httpClient.GetByteArrayAsync(url); - - var hash = bytes.AsSpan().xxHash3(); - var size = Size.FromLong(bytes.LongLength); - var streamFactory = new MemoryStreamFactory("cat.jpg".ToRelativePath(), new MemoryStream(bytes)); - - await archiveManager.BackupFiles(new ArchivedFileEntry[] - { - new(streamFactory, hash, size) - }); - - var hasFile = await archiveManager.HaveFile(hash); - hasFile.Should().BeTrue(); - - return hash; - } -} From bb8460298e3a57573de69dd77132bbbe90a304e3 Mon Sep 17 00:00:00 2001 From: erri120 Date: Mon, 16 Dec 2024 17:10:29 +0100 Subject: [PATCH 4/4] Use Match --- .../Step/GuidedInstallerStepViewModel.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Games/NexusMods.Games.FOMOD.UI/Step/GuidedInstallerStepViewModel.cs b/src/Games/NexusMods.Games.FOMOD.UI/Step/GuidedInstallerStepViewModel.cs index fdabe8dfe8..d29bc43ec0 100644 --- a/src/Games/NexusMods.Games.FOMOD.UI/Step/GuidedInstallerStepViewModel.cs +++ b/src/Games/NexusMods.Games.FOMOD.UI/Step/GuidedInstallerStepViewModel.cs @@ -63,16 +63,12 @@ public GuidedInstallerStepViewModel(IServiceProvider serviceProvider) { try { - if (optionImage.TryPickT0(out var uri, out var imageStoredFile)) - { - return await remoteImagePipeline.LoadResourceAsync(uri, CancellationToken.None); - } - else - { - return await fileImagePipeline.LoadResourceAsync(imageStoredFile.FileHash, CancellationToken.None); - } + return await optionImage.Match( + f0: uri => remoteImagePipeline.LoadResourceAsync(uri, CancellationToken.None), + f1: imageHash => fileImagePipeline.LoadResourceAsync(imageHash.FileHash, CancellationToken.None) + ); } - catch (Exception e) + catch (Exception) { return null; }