You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
249 lines
7.0 KiB
249 lines
7.0 KiB
7 months ago
|
using System;
|
||
|
using System.IO;
|
||
|
using System.Net.Http;
|
||
|
using System.Threading;
|
||
|
using System.Threading.Tasks;
|
||
|
using Avalonia;
|
||
|
using Avalonia.Controls;
|
||
|
using Avalonia.Controls.Metadata;
|
||
|
using Avalonia.Controls.Primitives;
|
||
|
using Avalonia.Interactivity;
|
||
|
using Avalonia.Labs.Controls;
|
||
|
using Avalonia.Media;
|
||
|
using Avalonia.Media.Imaging;
|
||
|
using Avalonia.Platform;
|
||
|
using Avalonia.Threading;
|
||
|
using StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
|
||
|
|
||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
|
||
|
|
||
|
/// <summary>
|
||
|
/// An image control that asynchronously retrieves an image using a <see cref="Uri"/>.
|
||
|
/// </summary>
|
||
|
[TemplatePart("PART_Image", typeof(Image))]
|
||
|
[TemplatePart("PART_PlaceholderImage", typeof(Image))]
|
||
|
public partial class BetterAsyncImage : TemplatedControl
|
||
|
{
|
||
|
protected Image? ImagePart { get; private set; }
|
||
|
protected Image? PlaceholderPart { get; private set; }
|
||
|
|
||
|
private bool _isInitialized;
|
||
|
private CancellationTokenSource? _tokenSource;
|
||
|
private AsyncImageState _state;
|
||
|
|
||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||
|
{
|
||
|
base.OnApplyTemplate(e);
|
||
|
|
||
|
ImagePart = e.NameScope.Get<Image>("PART_Image");
|
||
|
PlaceholderPart = e.NameScope.Get<Image>("PART_PlaceholderImage");
|
||
|
|
||
|
_tokenSource = new CancellationTokenSource();
|
||
|
|
||
|
_isInitialized = true;
|
||
|
|
||
|
if (Source != null)
|
||
|
{
|
||
|
SetSource(Source);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private async void SetSource(object? source)
|
||
|
{
|
||
|
if (!_isInitialized)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
_tokenSource?.Cancel();
|
||
|
|
||
|
_tokenSource = new CancellationTokenSource();
|
||
|
|
||
|
AttachSource(null);
|
||
|
|
||
|
if (source == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
State = AsyncImageState.Loading;
|
||
|
|
||
|
if (Source is IImage image)
|
||
|
{
|
||
|
AttachSource(image);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (Source == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var uri = Source;
|
||
|
|
||
|
if (uri != null && uri.IsAbsoluteUri)
|
||
|
{
|
||
|
if (uri.Scheme == "http" || uri.Scheme == "https")
|
||
|
{
|
||
|
Bitmap? bitmap = null;
|
||
|
// Android doesn't allow network requests on the main thread, even though we are using async apis.
|
||
|
#if NET6_0_OR_GREATER
|
||
|
if (OperatingSystem.IsAndroid())
|
||
|
{
|
||
|
await Task.Run(async () =>
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
bitmap = await LoadImageAsync(uri, _tokenSource.Token);
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||
|
{
|
||
|
State = AsyncImageState.Failed;
|
||
|
|
||
|
RaiseEvent(new AsyncImageFailedEventArgs(ex));
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
else
|
||
|
#endif
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
bitmap = await LoadImageAsync(uri, _tokenSource.Token);
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||
|
{
|
||
|
State = AsyncImageState.Failed;
|
||
|
|
||
|
RaiseEvent(new AsyncImageFailedEventArgs(ex));
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
AttachSource(bitmap);
|
||
|
}
|
||
|
else if (uri.Scheme == "avares")
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
AttachSource(new Bitmap(AssetLoader.Open(uri)));
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
State = AsyncImageState.Failed;
|
||
|
|
||
|
RaiseEvent(new AsyncImageFailedEventArgs(ex));
|
||
|
}
|
||
|
}
|
||
|
else if (uri.Scheme == "file" && File.Exists(uri.LocalPath))
|
||
|
{
|
||
|
// Added error handling here for local files
|
||
|
try
|
||
|
{
|
||
|
AttachSource(new Bitmap(uri.LocalPath));
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
State = AsyncImageState.Failed;
|
||
|
|
||
|
RaiseEvent(new AsyncImageFailedEventArgs(ex));
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
RaiseEvent(
|
||
|
new AsyncImageFailedEventArgs(
|
||
|
new UriFormatException($"Uri has unsupported scheme. Uri:{source}")
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
RaiseEvent(
|
||
|
new AsyncImageFailedEventArgs(
|
||
|
new UriFormatException($"Relative paths aren't supported. Uri:{source}")
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void AttachSource(IImage? image)
|
||
|
{
|
||
|
if (ImagePart != null)
|
||
|
{
|
||
|
ImagePart.Source = image;
|
||
|
}
|
||
|
|
||
|
_tokenSource?.Cancel();
|
||
|
_tokenSource = new CancellationTokenSource();
|
||
|
|
||
|
if (image == null)
|
||
|
{
|
||
|
State = AsyncImageState.Unloaded;
|
||
|
|
||
|
ImageTransition?.Start(ImagePart, PlaceholderPart, true, _tokenSource.Token);
|
||
|
}
|
||
|
else if (image.Size != default)
|
||
|
{
|
||
|
State = AsyncImageState.Loaded;
|
||
|
|
||
|
ImageTransition?.Start(PlaceholderPart, ImagePart, true, _tokenSource.Token);
|
||
|
|
||
|
RaiseEvent(new RoutedEventArgs(OpenedEvent));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private async Task<Bitmap> LoadImageAsync(Uri? url, CancellationToken token)
|
||
|
{
|
||
|
if (await ProvideCachedResourceAsync(url, token) is { } bitmap)
|
||
|
{
|
||
|
return bitmap;
|
||
|
}
|
||
|
#if NET6_0_OR_GREATER
|
||
|
using var client = new HttpClient();
|
||
|
var stream = await client.GetStreamAsync(url, token).ConfigureAwait(false);
|
||
|
|
||
|
await using var memoryStream = new MemoryStream();
|
||
|
await stream.CopyToAsync(memoryStream, token).ConfigureAwait(false);
|
||
|
#elif NETSTANDARD2_0
|
||
|
using var client = new HttpClient();
|
||
|
var response = await client.GetAsync(url, token).ConfigureAwait(false);
|
||
|
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||
|
|
||
|
using var memoryStream = new MemoryStream();
|
||
|
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||
|
#endif
|
||
|
|
||
|
memoryStream.Position = 0;
|
||
|
return new Bitmap(memoryStream);
|
||
|
}
|
||
|
|
||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||
|
{
|
||
|
base.OnPropertyChanged(change);
|
||
|
|
||
|
if (change.Property == SourceProperty)
|
||
|
{
|
||
|
SetSource(Source);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected virtual async Task<Bitmap?> ProvideCachedResourceAsync(Uri? imageUri, CancellationToken token)
|
||
|
{
|
||
|
if (IsCacheEnabled && imageUri != null)
|
||
|
{
|
||
|
return await ImageCache
|
||
|
.Instance.GetFromCacheAsync(imageUri, cancellationToken: token)
|
||
|
.ConfigureAwait(false);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
}
|