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.
248 lines
7.0 KiB
248 lines
7.0 KiB
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; |
|
} |
|
}
|
|
|