mirror of
https://github.com/yaobiao131/downkyicore.git
synced 2025-08-10 00:52:31 +00:00
fix: 优化项目代码
1、修复部分字幕下载问题 2、修复自定义aria2设置出错问题
This commit is contained in:
15
DownKyi/CustomControl/AsyncImageLoader/IAsyncImageLoader.cs
Normal file
15
DownKyi/CustomControl/AsyncImageLoader/IAsyncImageLoader.cs
Normal 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);
|
||||
}
|
||||
73
DownKyi/CustomControl/AsyncImageLoader/ImageBrushLoader.cs
Normal file
73
DownKyi/CustomControl/AsyncImageLoader/ImageBrushLoader.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
107
DownKyi/CustomControl/AsyncImageLoader/ImageLoader.cs
Normal file
107
DownKyi/CustomControl/AsyncImageLoader/ImageLoader.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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("-", "");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user