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; /// /// An image control that asynchronously retrieves an image using a . /// [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("PART_Image"); PlaceholderPart = e.NameScope.Get("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 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 ProvideCachedResourceAsync(Uri? imageUri, CancellationToken token) { if (IsCacheEnabled && imageUri != null) { return await ImageCache .Instance.GetFromCacheAsync(imageUri, cancellationToken: token) .ConfigureAwait(false); } return null; } }