fix: 优化项目代码

1、修复部分字幕下载问题
2、修复自定义aria2设置出错问题
This commit is contained in:
yaobiao131
2025-03-13 13:57:06 +08:00
parent 2c349e55c7
commit f744d264e0
175 changed files with 3926 additions and 5563 deletions

View File

@@ -0,0 +1,15 @@
using System;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace DownKyi.CustomControl.AsyncImageLoader;
public interface IAsyncImageLoader : IDisposable
{
/// <summary>
/// Loads image
/// </summary>
/// <param name="url">Target url</param>
/// <returns>Bitmap</returns>
public Task<Bitmap?> ProvideImageAsync(string url);
}

View File

@@ -0,0 +1,73 @@
using System;
using System.IO;
using Avalonia;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using DownKyi.Core.Storage;
using DownKyi.CustomControl.AsyncImageLoader.Loaders;
namespace DownKyi.CustomControl.AsyncImageLoader;
public static class ImageBrushLoader
{
private static readonly ParametrizedLogger? Logger;
public static IAsyncImageLoader AsyncImageLoader { get; set; } = new DiskCachedWebImageLoader(Path.Combine(StorageManager.GetCache(), "Images"));
static ImageBrushLoader()
{
SourceProperty.Changed.AddClassHandler<ImageBrush>(OnSourceChanged);
Logger = Avalonia.Logging.Logger.TryGet(LogEventLevel.Error, ImageLoader.AsyncImageLoaderLogArea);
}
private static async void OnSourceChanged(ImageBrush imageBrush, AvaloniaPropertyChangedEventArgs args)
{
var (oldValue, newValue) = args.GetOldAndNewValue<string?>();
if (oldValue == newValue)
return;
SetIsLoading(imageBrush, true);
Bitmap? bitmap = null;
try
{
if (newValue is not null)
{
bitmap = await AsyncImageLoader.ProvideImageAsync(newValue);
}
}
catch (Exception e)
{
Logger?.Log("ImageBrushLoader", "ImageBrushLoader image resolution failed: {0}", e);
}
if (GetSource(imageBrush) != newValue) return;
imageBrush.Source = bitmap;
SetIsLoading(imageBrush, false);
}
public static readonly AttachedProperty<string?> SourceProperty = AvaloniaProperty.RegisterAttached<ImageBrush, string?>("Source", typeof(ImageLoader));
public static string? GetSource(ImageBrush element)
{
return element.GetValue(SourceProperty);
}
public static void SetSource(ImageBrush element, string? value)
{
element.SetValue(SourceProperty, value);
}
public static readonly AttachedProperty<bool> IsLoadingProperty = AvaloniaProperty.RegisterAttached<ImageBrush, bool>("IsLoading", typeof(ImageLoader));
public static bool GetIsLoading(ImageBrush element)
{
return element.GetValue(IsLoadingProperty);
}
private static void SetIsLoading(ImageBrush element, bool value)
{
element.SetValue(IsLoadingProperty, value);
}
}

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Logging;
using Avalonia.Media.Imaging;
using DownKyi.Core.Storage;
using DownKyi.CustomControl.AsyncImageLoader.Loaders;
namespace DownKyi.CustomControl.AsyncImageLoader;
public static class ImageLoader
{
private static readonly ParametrizedLogger? Logger;
public const string AsyncImageLoaderLogArea = "AsyncImageLoader";
public static readonly AttachedProperty<string?> SourceProperty =
AvaloniaProperty.RegisterAttached<Image, string?>("Source", typeof(ImageLoader));
public static readonly AttachedProperty<bool> IsLoadingProperty =
AvaloniaProperty.RegisterAttached<Image, bool>("IsLoading", typeof(ImageLoader));
static ImageLoader()
{
SourceProperty.Changed.AddClassHandler<Image>(OnSourceChanged);
Logger = Avalonia.Logging.Logger.TryGet(LogEventLevel.Error, AsyncImageLoaderLogArea);
}
public static IAsyncImageLoader AsyncImageLoader { get; set; } = new DiskCachedWebImageLoader(Path.Combine(StorageManager.GetCache(), "Images"));
private static readonly ConcurrentDictionary<Image, CancellationTokenSource> PendingOperations = new();
private static async void OnSourceChanged(Image sender, AvaloniaPropertyChangedEventArgs args)
{
var url = args.GetNewValue<string?>();
var cts = PendingOperations.AddOrUpdate(sender, new CancellationTokenSource(), (x, y) =>
{
y.Cancel();
return new CancellationTokenSource();
}
);
if (url == null)
{
((ICollection<KeyValuePair<Image, CancellationTokenSource>>)PendingOperations).Remove(new KeyValuePair<Image, CancellationTokenSource>(sender, cts));
sender.Source = null;
return;
}
SetIsLoading(sender, true);
var bitmap = await Task.Run(async () =>
{
try
{
// A small delay allows to cancel early if the image goes out of screen too fast (eg. scrolling)
// The Bitmap constructor is expensive and cannot be cancelled
await Task.Delay(10, cts.Token);
return await AsyncImageLoader.ProvideImageAsync(url);
}
catch (TaskCanceledException)
{
return null;
}
catch (Exception e)
{
Logger?.Log(LogEventLevel.Error, "ImageLoader image resolution failed: {0}", e);
return null;
}
});
if (bitmap != null && !cts.Token.IsCancellationRequested)
sender.Source = bitmap!;
// "It is not guaranteed to be thread safe by ICollection, but ConcurrentDictionary's implementation is. Additionally, we recently exposed this API for .NET 5 as a public ConcurrentDictionary.TryRemove"
((ICollection<KeyValuePair<Image, CancellationTokenSource>>)PendingOperations).Remove(new KeyValuePair<Image, CancellationTokenSource>(sender, cts));
SetIsLoading(sender, false);
}
public static string? GetSource(Image element)
{
return element.GetValue(SourceProperty);
}
public static void SetSource(Image element, string? value)
{
element.SetValue(SourceProperty, value);
}
public static bool GetIsLoading(Image element)
{
return element.GetValue(IsLoadingProperty);
}
private static void SetIsLoading(Image element, bool value)
{
element.SetValue(IsLoadingProperty, value);
}
}

View File

@@ -0,0 +1,20 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:DownKyi.CustomControl.AsyncImageLoader.Loaders">
<Design.PreviewWith>
<controls:AdvancedImage />
</Design.PreviewWith>
<Style Selector="controls|AdvancedImage">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<!-- CurrentImage will be rendered with codebehind, just as it is done in the Image -->
<ProgressBar VerticalAlignment="Center" MinWidth="0" MaxWidth="100"
IsIndeterminate="True"
IsVisible="{TemplateBinding IsLoading}" />
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

View File

@@ -0,0 +1,339 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Logging;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace DownKyi.CustomControl.AsyncImageLoader.Loaders;
public class AdvancedImage : ContentControl
{
/// <summary>
/// Defines the <see cref="Loader" /> property.
/// </summary>
public static readonly StyledProperty<IAsyncImageLoader?> LoaderProperty = AvaloniaProperty.Register<AdvancedImage, IAsyncImageLoader?>(nameof(Loader));
/// <summary>
/// Defines the <see cref="Source" /> property.
/// </summary>
public static readonly StyledProperty<string?> SourceProperty = AvaloniaProperty.Register<AdvancedImage, string?>(nameof(Source));
/// <summary>
/// Defines the <see cref="ShouldLoaderChangeTriggerUpdate" /> property.
/// </summary>
public static readonly DirectProperty<AdvancedImage, bool> ShouldLoaderChangeTriggerUpdateProperty =
AvaloniaProperty.RegisterDirect<AdvancedImage, bool>(
nameof(ShouldLoaderChangeTriggerUpdate),
image => image._shouldLoaderChangeTriggerUpdate,
(image, b) => image._shouldLoaderChangeTriggerUpdate = b
);
/// <summary>
/// Defines the <see cref="IsLoading" /> property.
/// </summary>
public static readonly DirectProperty<AdvancedImage, bool> IsLoadingProperty =
AvaloniaProperty.RegisterDirect<AdvancedImage, bool>(
nameof(IsLoading),
image => image._isLoading);
/// <summary>
/// Defines the <see cref="CurrentImage" /> property.
/// </summary>
public static readonly DirectProperty<AdvancedImage, IImage?> CurrentImageProperty =
AvaloniaProperty.RegisterDirect<AdvancedImage, IImage?>(
nameof(CurrentImage),
image => image._currentImage);
/// <summary>
/// Defines the <see cref="Stretch" /> property.
/// </summary>
public static readonly StyledProperty<Stretch> StretchProperty =
Image.StretchProperty.AddOwner<AdvancedImage>();
/// <summary>
/// Defines the <see cref="StretchDirection" /> property.
/// </summary>
public static readonly StyledProperty<StretchDirection> StretchDirectionProperty =
Image.StretchDirectionProperty.AddOwner<AdvancedImage>();
private readonly Uri? _baseUri;
private RoundedRect _cornerRadiusClip;
private IImage? _currentImage;
private bool _isCornerRadiusUsed;
private bool _isLoading;
private bool _shouldLoaderChangeTriggerUpdate;
private CancellationTokenSource? _updateCancellationToken;
private readonly ParametrizedLogger? _logger;
static AdvancedImage()
{
AffectsRender<AdvancedImage>(CurrentImageProperty, StretchProperty, StretchDirectionProperty,
CornerRadiusProperty);
AffectsMeasure<AdvancedImage>(CurrentImageProperty, StretchProperty, StretchDirectionProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="AdvancedImage" /> class.
/// </summary>
/// <param name="baseUri">The base URL for the XAML context.</param>
public AdvancedImage(Uri? baseUri)
{
_baseUri = baseUri;
_logger = Logger.TryGet(LogEventLevel.Error, ImageLoader.AsyncImageLoaderLogArea);
}
/// <summary>
/// Initializes a new instance of the <see cref="AdvancedImage" /> class.
/// </summary>
/// <param name="serviceProvider">The XAML service provider.</param>
public AdvancedImage(IServiceProvider serviceProvider)
: this((serviceProvider.GetService(typeof(IUriContext)) as IUriContext)?.BaseUri)
{
}
/// <summary>
/// Gets or sets the URI for image that will be displayed.
/// </summary>
public IAsyncImageLoader? Loader
{
get => GetValue(LoaderProperty);
set => SetValue(LoaderProperty, value);
}
/// <summary>
/// Gets or sets the URI for image that will be displayed.
/// </summary>
public string? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary>
/// Gets or sets the value controlling whether the image should be reloaded after changing the loader.
/// </summary>
public bool ShouldLoaderChangeTriggerUpdate
{
get => _shouldLoaderChangeTriggerUpdate;
set => SetAndRaise(ShouldLoaderChangeTriggerUpdateProperty, ref _shouldLoaderChangeTriggerUpdate, value);
}
/// <summary>
/// Gets a value indicating is image currently is loading state.
/// </summary>
public bool IsLoading
{
get => _isLoading;
private set => SetAndRaise(IsLoadingProperty, ref _isLoading, value);
}
/// <summary>
/// Gets a currently loaded IImage.
/// </summary>
public IImage? CurrentImage
{
get => _currentImage;
set => SetAndRaise(CurrentImageProperty, ref _currentImage, value);
}
/// <summary>
/// Gets or sets a value controlling how the image will be stretched.
/// </summary>
public Stretch Stretch
{
get => GetValue(StretchProperty);
set => SetValue(StretchProperty, value);
}
/// <summary>
/// Gets or sets a value controlling in what direction the image will be stretched.
/// </summary>
public StretchDirection StretchDirection
{
get => GetValue(StretchDirectionProperty);
set => SetValue(StretchDirectionProperty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == SourceProperty)
UpdateImage(change.GetNewValue<string>(), Loader);
else if (change.Property == LoaderProperty && ShouldLoaderChangeTriggerUpdate)
UpdateImage(change.GetNewValue<string>(), Loader);
else if (change.Property == CurrentImageProperty)
ClearSourceIfUserProvideImage();
else if (change.Property == CornerRadiusProperty)
UpdateCornerRadius(change.GetNewValue<CornerRadius>());
else if (change.Property == BoundsProperty && CornerRadius != default) UpdateCornerRadius(CornerRadius);
base.OnPropertyChanged(change);
}
private void ClearSourceIfUserProvideImage()
{
if (CurrentImage is not null and not ImageWrapper)
{
// User provided image himself
Source = null;
}
}
private async void UpdateImage(string? source, IAsyncImageLoader? loader)
{
var cancellationTokenSource = new CancellationTokenSource();
var oldCancellationToken = Interlocked.Exchange(ref _updateCancellationToken, cancellationTokenSource);
try
{
oldCancellationToken?.Cancel();
}
catch (ObjectDisposedException)
{
}
if (source is null && CurrentImage is not ImageWrapper)
{
// User provided image himself
return;
}
IsLoading = true;
CurrentImage = null;
var bitmap = await Task.Run(async () =>
{
try
{
if (source == null)
return null;
// A small delay allows to cancel early if the image goes out of screen too fast (eg. scrolling)
// The Bitmap constructor is expensive and cannot be cancelled
await Task.Delay(10, cancellationTokenSource.Token);
// Hack to support relative URI
// TODO: Refactor IAsyncImageLoader to support BaseUri
try
{
var uri = new Uri(source, UriKind.RelativeOrAbsolute);
if (AssetLoader.Exists(uri, _baseUri))
return new Bitmap(AssetLoader.Open(uri, _baseUri));
}
catch (Exception)
{
// ignored
}
loader ??= ImageLoader.AsyncImageLoader;
return await loader.ProvideImageAsync(source);
}
catch (TaskCanceledException)
{
return null;
}
catch (Exception e)
{
_logger?.Log(this, "AdvancedImage image resolution failed: {0}", e);
return null;
}
finally
{
cancellationTokenSource.Dispose();
}
}, CancellationToken.None);
if (cancellationTokenSource.IsCancellationRequested)
return;
CurrentImage = bitmap is null ? null : new ImageWrapper(bitmap);
IsLoading = false;
}
private void UpdateCornerRadius(CornerRadius radius)
{
_isCornerRadiusUsed = radius != default;
_cornerRadiusClip = new RoundedRect(new Rect(0, 0, Bounds.Width, Bounds.Height), radius);
}
/// <summary>
/// Renders the control.
/// </summary>
/// <param name="context">The drawing context.</param>
public override void Render(DrawingContext context)
{
var source = CurrentImage;
if (source != null && Bounds is { Width: > 0, Height: > 0 })
{
var viewPort = new Rect(Bounds.Size);
var sourceSize = source.Size;
var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
var scaledSize = sourceSize * scale;
var destRect = viewPort
.CenterRect(new Rect(scaledSize))
.Intersect(viewPort);
var sourceRect = new Rect(sourceSize)
.CenterRect(new Rect(destRect.Size / scale));
DrawingContext.PushedState? pushedState =
_isCornerRadiusUsed ? context.PushClip(_cornerRadiusClip) : null;
context.DrawImage(source, sourceRect, destRect);
pushedState?.Dispose();
}
else
{
base.Render(context);
}
}
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>The desired size of the control.</returns>
protected override Size MeasureOverride(Size availableSize)
{
return CurrentImage != null
? Stretch.CalculateSize(availableSize, CurrentImage.Size, StretchDirection)
: base.MeasureOverride(availableSize);
}
/// <inheritdoc />
protected override Size ArrangeOverride(Size finalSize)
{
return CurrentImage != null
? Stretch.CalculateSize(finalSize, CurrentImage.Size)
: base.ArrangeOverride(finalSize);
}
public sealed class ImageWrapper : IImage
{
public IImage ImageImplementation { get; }
internal ImageWrapper(IImage imageImplementation)
{
ImageImplementation = imageImplementation;
}
/// <inheritdoc />
public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
{
ImageImplementation.Draw(context, sourceRect, destRect);
}
/// <inheritdoc />
public Size Size => ImageImplementation.Size;
}
}

View File

@@ -0,0 +1,177 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace DownKyi.CustomControl.AsyncImageLoader.Loaders;
public class BaseWebImageLoader : IAsyncImageLoader
{
private readonly ParametrizedLogger? _logger;
private readonly bool _shouldDisposeHttpClient;
/// <summary>
/// Initializes a new instance with new <see cref="HttpClient" /> instance
/// </summary>
public BaseWebImageLoader() : this(new HttpClient(), true)
{
}
/// <summary>
/// Initializes a new instance with the provided <see cref="HttpClient" />, and specifies whether that
/// <see cref="HttpClient" /> should be disposed when this instance is disposed.
/// </summary>
/// <param name="httpClient">The HttpMessageHandler responsible for processing the HTTP response messages.</param>
/// <param name="disposeHttpClient">
/// true if the inner handler should be disposed of by Dispose; false if you intend to
/// reuse the HttpClient.
/// </param>
public BaseWebImageLoader(HttpClient httpClient, bool disposeHttpClient)
{
HttpClient = httpClient;
_shouldDisposeHttpClient = disposeHttpClient;
_logger = Logger.TryGet(LogEventLevel.Error, ImageLoader.AsyncImageLoaderLogArea);
}
protected HttpClient HttpClient { get; }
/// <inheritdoc />
public virtual async Task<Bitmap?> ProvideImageAsync(string url)
{
return await LoadAsync(url).ConfigureAwait(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Attempts to load bitmap
/// </summary>
/// <param name="url">Target url</param>
/// <returns>Bitmap</returns>
protected virtual async Task<Bitmap?> LoadAsync(string url)
{
var internalOrCachedBitmap =
await LoadFromLocalAsync(url).ConfigureAwait(false)
?? await LoadFromInternalAsync(url).ConfigureAwait(false)
?? await LoadFromGlobalCache(url).ConfigureAwait(false);
if (internalOrCachedBitmap != null) return internalOrCachedBitmap;
try
{
var externalBytes = await LoadDataFromExternalAsync(url).ConfigureAwait(false);
if (externalBytes == null) return null;
using var memoryStream = new MemoryStream(externalBytes);
var bitmap = new Bitmap(memoryStream);
await SaveToGlobalCache(url, externalBytes).ConfigureAwait(false);
return bitmap;
}
catch (Exception e)
{
_logger?.Log(this, "Failed to resolve image: {RequestUri}\nException: {Exception}", url, e);
return null;
}
}
/// <summary>
/// the url maybe is local file url,so if file exists ,we got a Bitmap
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private Task<Bitmap?> LoadFromLocalAsync(string url)
{
return Task.FromResult(File.Exists(url) ? new Bitmap(url) : null);
}
/// <summary>
/// Receives image bytes from an internal source (for example, from the disk).
/// This data will be NOT cached globally (because it is assumed that it is already in internal source us and does not
/// require global caching)
/// </summary>
/// <param name="url">Target url</param>
/// <returns>Bitmap</returns>
protected virtual Task<Bitmap?> LoadFromInternalAsync(string url)
{
try
{
var uri = url.StartsWith("/")
? new Uri(url, UriKind.Relative)
: new Uri(url, UriKind.RelativeOrAbsolute);
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
return Task.FromResult<Bitmap?>(null);
if (uri is { IsAbsoluteUri: true, IsFile: true })
return Task.FromResult(new Bitmap(uri.LocalPath))!;
return Task.FromResult(new Bitmap(AssetLoader.Open(uri)))!;
}
catch (Exception e)
{
_logger?.Log(this,
"Failed to resolve image from request with uri: {RequestUri}\nException: {Exception}", url, e);
return Task.FromResult<Bitmap?>(null);
}
}
/// <summary>
/// Receives image bytes from an external source (for example, from the Internet).
/// This data will be cached globally (if required by the current implementation)
/// </summary>
/// <param name="url">Target url</param>
/// <returns>Image bytes</returns>
protected virtual async Task<byte[]?> LoadDataFromExternalAsync(string url)
{
try
{
return await HttpClient.GetByteArrayAsync(url).ConfigureAwait(false);
}
catch (Exception e)
{
_logger?.Log(this,
"Failed to resolve image from request with uri: {RequestUri}\nException: {Exception}", url, e);
return null;
}
}
/// <summary>
/// Attempts to load image from global cache (if it is stored before)
/// </summary>
/// <param name="url">Target url</param>
/// <returns>Bitmap</returns>
protected virtual Task<Bitmap?> LoadFromGlobalCache(string url)
{
// Current implementation does not provide global caching
return Task.FromResult<Bitmap?>(null);
}
/// <summary>
/// Attempts to load image from global cache (if it is stored before)
/// </summary>
/// <param name="url">Target url</param>
/// <param name="imageBytes">Bytes to save</param>
/// <returns>Bitmap</returns>
protected virtual Task SaveToGlobalCache(string url, byte[] imageBytes)
{
// Current implementation does not provide global caching
return Task.CompletedTask;
}
~BaseWebImageLoader()
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (disposing && _shouldDisposeHttpClient) HttpClient.Dispose();
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace DownKyi.CustomControl.AsyncImageLoader.Loaders;
public class DiskCachedWebImageLoader : BaseWebImageLoader
{
private readonly string _cacheFolder;
public DiskCachedWebImageLoader(string cacheFolder = "Cache/Images/")
{
_cacheFolder = cacheFolder;
}
public DiskCachedWebImageLoader(HttpClient httpClient, bool disposeHttpClient, string cacheFolder = "Cache/Images/") : base(httpClient, disposeHttpClient)
{
_cacheFolder = cacheFolder;
}
/// <inheritdoc />
protected override Task<Bitmap?> LoadFromGlobalCache(string url)
{
var path = Path.Combine(_cacheFolder, CreateMd5(url));
return File.Exists(path) ? Task.FromResult<Bitmap?>(new Bitmap(path)) : Task.FromResult<Bitmap?>(null);
}
#if NETSTANDARD2_1
protected override async Task SaveToGlobalCache(string url, byte[] imageBytes) {
var path = Path.Combine(_cacheFolder, CreateMd5(url));
Directory.CreateDirectory(_cacheFolder);
await File.WriteAllBytesAsync(path, imageBytes).ConfigureAwait(false);
}
#else
protected override Task SaveToGlobalCache(string url, byte[] imageBytes)
{
var path = Path.Combine(_cacheFolder, CreateMd5(url));
Directory.CreateDirectory(_cacheFolder);
File.WriteAllBytes(path, imageBytes);
return Task.CompletedTask;
}
#endif
protected static string CreateMd5(string input)
{
// Use input string to calculate MD5 hash
using var md5 = MD5.Create();
var inputBytes = Encoding.ASCII.GetBytes(input);
var hashBytes = md5.ComputeHash(inputBytes);
// Convert the byte array to hexadecimal string
return BitConverter.ToString(hashBytes).Replace("-", "");
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Concurrent;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace DownKyi.CustomControl.AsyncImageLoader.Loaders;
public class RamCachedWebImageLoader : BaseWebImageLoader
{
private readonly ConcurrentDictionary<string, Task<Bitmap?>> _memoryCache = new();
/// <inheritdoc />
public RamCachedWebImageLoader()
{
}
/// <inheritdoc />
public RamCachedWebImageLoader(HttpClient httpClient, bool disposeHttpClient) : base(httpClient, disposeHttpClient)
{
}
/// <inheritdoc />
public override async Task<Bitmap?> ProvideImageAsync(string url)
{
var bitmap = await _memoryCache.GetOrAdd(url, LoadAsync).ConfigureAwait(false);
// If load failed - remove from cache and return
// Next load attempt will try to load image again
if (bitmap == null) _memoryCache.TryRemove(url, out _);
return bitmap;
}
}