JT
7 months ago
committed by
GitHub
31 changed files with 1515 additions and 39 deletions
@ -0,0 +1,20 @@ |
|||||||
|
using System; |
||||||
|
using Avalonia.Interactivity; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs; |
||||||
|
|
||||||
|
public partial class BetterAsyncImage |
||||||
|
{ |
||||||
|
public class AsyncImageFailedEventArgs : RoutedEventArgs |
||||||
|
{ |
||||||
|
internal AsyncImageFailedEventArgs(Exception? errorException = null, string errorMessage = "") |
||||||
|
: base(FailedEvent) |
||||||
|
{ |
||||||
|
ErrorException = errorException; |
||||||
|
ErrorMessage = errorMessage; |
||||||
|
} |
||||||
|
|
||||||
|
public Exception? ErrorException { get; private set; } |
||||||
|
public string ErrorMessage { get; private set; } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
using System; |
||||||
|
using Avalonia.Interactivity; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs; |
||||||
|
|
||||||
|
public partial class BetterAsyncImage |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Deines the <see cref="Opened"/> event |
||||||
|
/// </summary> |
||||||
|
public static readonly RoutedEvent<RoutedEventArgs> OpenedEvent = RoutedEvent.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
RoutedEventArgs |
||||||
|
>(nameof(Opened), RoutingStrategies.Bubble); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Deines the <see cref="Failed"/> event |
||||||
|
/// </summary> |
||||||
|
public static readonly RoutedEvent<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs> FailedEvent = |
||||||
|
RoutedEvent.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs |
||||||
|
>(nameof(Failed), RoutingStrategies.Bubble); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Occurs when the image is successfully loaded. |
||||||
|
/// </summary> |
||||||
|
public event EventHandler<RoutedEventArgs>? Opened |
||||||
|
{ |
||||||
|
add => AddHandler(OpenedEvent, value); |
||||||
|
remove => RemoveHandler(OpenedEvent, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Occurs when the image fails to load the uri provided. |
||||||
|
/// </summary> |
||||||
|
public event EventHandler<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs>? Failed |
||||||
|
{ |
||||||
|
add => AddHandler(FailedEvent, value); |
||||||
|
remove => RemoveHandler(FailedEvent, value); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,135 @@ |
|||||||
|
using System; |
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Animation; |
||||||
|
using Avalonia.Labs.Controls; |
||||||
|
using Avalonia.Media; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs; |
||||||
|
|
||||||
|
public partial class BetterAsyncImage |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="PlaceholderSource"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly StyledProperty<IImage?> PlaceholderSourceProperty = AvaloniaProperty.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
IImage? |
||||||
|
>(nameof(PlaceholderSource)); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="Source"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly StyledProperty<Uri?> SourceProperty = AvaloniaProperty.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
Uri? |
||||||
|
>(nameof(Source)); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="Stretch"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
Stretch |
||||||
|
>(nameof(Stretch), Stretch.Uniform); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="PlaceholderStretch"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly StyledProperty<Stretch> PlaceholderStretchProperty = AvaloniaProperty.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
Stretch |
||||||
|
>(nameof(PlaceholderStretch), Stretch.Uniform); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="State"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly DirectProperty<BetterAsyncImage, AsyncImageState> StateProperty = |
||||||
|
AvaloniaProperty.RegisterDirect<BetterAsyncImage, AsyncImageState>( |
||||||
|
nameof(State), |
||||||
|
o => o.State, |
||||||
|
(o, v) => o.State = v |
||||||
|
); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="ImageTransition"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly StyledProperty<IPageTransition?> ImageTransitionProperty = |
||||||
|
AvaloniaProperty.Register<BetterAsyncImage, IPageTransition?>( |
||||||
|
nameof(ImageTransition), |
||||||
|
new CrossFade(TimeSpan.FromSeconds(0.25)) |
||||||
|
); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="IsCacheEnabled"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly DirectProperty<BetterAsyncImage, bool> IsCacheEnabledProperty = |
||||||
|
AvaloniaProperty.RegisterDirect<BetterAsyncImage, bool>( |
||||||
|
nameof(IsCacheEnabled), |
||||||
|
o => o.IsCacheEnabled, |
||||||
|
(o, v) => o.IsCacheEnabled = v |
||||||
|
); |
||||||
|
private bool _isCacheEnabled; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the placeholder image. |
||||||
|
/// </summary> |
||||||
|
public IImage? PlaceholderSource |
||||||
|
{ |
||||||
|
get => GetValue(PlaceholderSourceProperty); |
||||||
|
set => SetValue(PlaceholderSourceProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the uri pointing to the image resource |
||||||
|
/// </summary> |
||||||
|
public Uri? Source |
||||||
|
{ |
||||||
|
get => GetValue(SourceProperty); |
||||||
|
set => SetValue(SourceProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets a value controlling how the image will be stretched. |
||||||
|
/// </summary> |
||||||
|
public Stretch Stretch |
||||||
|
{ |
||||||
|
get { return GetValue(StretchProperty); } |
||||||
|
set { SetValue(StretchProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets a value controlling how the placeholder will be stretched. |
||||||
|
/// </summary> |
||||||
|
public Stretch PlaceholderStretch |
||||||
|
{ |
||||||
|
get { return GetValue(StretchProperty); } |
||||||
|
set { SetValue(StretchProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets the current loading state of the image. |
||||||
|
/// </summary> |
||||||
|
public AsyncImageState State |
||||||
|
{ |
||||||
|
get => _state; |
||||||
|
private set => SetAndRaise(StateProperty, ref _state, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the transition to run when the image is loaded. |
||||||
|
/// </summary> |
||||||
|
public IPageTransition? ImageTransition |
||||||
|
{ |
||||||
|
get => GetValue(ImageTransitionProperty); |
||||||
|
set => SetValue(ImageTransitionProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets whether to use cache for retrieved images |
||||||
|
/// </summary> |
||||||
|
public bool IsCacheEnabled |
||||||
|
{ |
||||||
|
get => _isCacheEnabled; |
||||||
|
set => SetAndRaise(IsCacheEnabledProperty, ref _isCacheEnabled, value); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,248 @@ |
|||||||
|
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; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,565 @@ |
|||||||
|
// Parts of this file was taken from Windows Community Toolkit CacheBase implementation |
||||||
|
// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp.UI/Cache/ImageCache.cs |
||||||
|
|
||||||
|
// Licensed to the .NET Foundation under one or more agreements. |
||||||
|
// The .NET Foundation licenses this file to you under the MIT license. |
||||||
|
// See the LICENSE file in the project root for more information. |
||||||
|
|
||||||
|
using System; |
||||||
|
using System.Collections.Concurrent; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Net.Http; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
internal abstract class CacheBase<T> |
||||||
|
{ |
||||||
|
private class ConcurrentRequest |
||||||
|
{ |
||||||
|
public Task<T?>? Task { get; set; } |
||||||
|
|
||||||
|
public bool EnsureCachedCopy { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
private readonly SemaphoreSlim _cacheFolderSemaphore = new SemaphoreSlim(1); |
||||||
|
private string? _baseFolder = null; |
||||||
|
private string? _cacheFolderName = null; |
||||||
|
|
||||||
|
private string? _cacheFolder = null; |
||||||
|
private InMemoryStorage<T>? _inMemoryFileStorage = null; |
||||||
|
|
||||||
|
private ConcurrentDictionary<string, ConcurrentRequest> _concurrentTasks = |
||||||
|
new ConcurrentDictionary<string, ConcurrentRequest>(); |
||||||
|
|
||||||
|
private HttpClient? _httpClient = null; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Initializes a new instance of the <see cref="CacheBase{T}"/> class. |
||||||
|
/// </summary> |
||||||
|
protected CacheBase() |
||||||
|
{ |
||||||
|
var options = CacheOptions.Default; |
||||||
|
CacheDuration = options?.CacheDuration ?? TimeSpan.FromDays(1); |
||||||
|
_baseFolder = options?.BaseCachePath ?? null; |
||||||
|
_inMemoryFileStorage = new InMemoryStorage<T>(); |
||||||
|
RetryCount = 1; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the life duration of every cache entry. |
||||||
|
/// </summary> |
||||||
|
public TimeSpan CacheDuration { get; set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the number of retries trying to ensure the file is cached. |
||||||
|
/// </summary> |
||||||
|
public uint RetryCount { get; set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets max in-memory item storage count |
||||||
|
/// </summary> |
||||||
|
public int MaxMemoryCacheCount |
||||||
|
{ |
||||||
|
get { return _inMemoryFileStorage?.MaxItemCount ?? 0; } |
||||||
|
set |
||||||
|
{ |
||||||
|
if (_inMemoryFileStorage != null) |
||||||
|
_inMemoryFileStorage.MaxItemCount = value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets instance of <see cref="HttpClient"/> |
||||||
|
/// </summary> |
||||||
|
protected HttpClient HttpClient |
||||||
|
{ |
||||||
|
get |
||||||
|
{ |
||||||
|
if (_httpClient == null) |
||||||
|
{ |
||||||
|
var messageHandler = new HttpClientHandler(); |
||||||
|
|
||||||
|
_httpClient = new HttpClient(messageHandler); |
||||||
|
} |
||||||
|
|
||||||
|
return _httpClient; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Initializes FileCache and provides root folder and cache folder name |
||||||
|
/// </summary> |
||||||
|
/// <param name="folder">Folder that is used as root for cache</param> |
||||||
|
/// <param name="folderName">Cache folder name</param> |
||||||
|
/// <param name="httpMessageHandler">instance of <see cref="HttpMessageHandler"/></param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
public virtual async Task InitializeAsync( |
||||||
|
string? folder = null, |
||||||
|
string? folderName = null, |
||||||
|
HttpMessageHandler? httpMessageHandler = null |
||||||
|
) |
||||||
|
{ |
||||||
|
_baseFolder = folder; |
||||||
|
_cacheFolderName = folderName; |
||||||
|
|
||||||
|
_cacheFolder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
|
||||||
|
if (httpMessageHandler != null) |
||||||
|
{ |
||||||
|
_httpClient = new HttpClient(httpMessageHandler); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Clears all files in the cache |
||||||
|
/// </summary> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
public async Task ClearAsync() |
||||||
|
{ |
||||||
|
var folder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
var files = Directory.EnumerateFiles(folder!); |
||||||
|
|
||||||
|
await InternalClearAsync(files.Select(x => x as string)).ConfigureAwait(false); |
||||||
|
|
||||||
|
_inMemoryFileStorage?.Clear(); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Clears file if it has expired |
||||||
|
/// </summary> |
||||||
|
/// <param name="duration">timespan to compute whether file has expired or not</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
public Task ClearAsync(TimeSpan duration) |
||||||
|
{ |
||||||
|
return RemoveExpiredAsync(duration); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Removes cached files that have expired |
||||||
|
/// </summary> |
||||||
|
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
public async Task RemoveExpiredAsync(TimeSpan? duration = null) |
||||||
|
{ |
||||||
|
TimeSpan expiryDuration = duration ?? CacheDuration; |
||||||
|
|
||||||
|
var folder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
var files = Directory.EnumerateFiles(folder!); |
||||||
|
|
||||||
|
var filesToDelete = new List<string>(); |
||||||
|
|
||||||
|
foreach (var file in files) |
||||||
|
{ |
||||||
|
if (file == null) |
||||||
|
{ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false)) |
||||||
|
{ |
||||||
|
filesToDelete.Add(file); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
await InternalClearAsync(filesToDelete).ConfigureAwait(false); |
||||||
|
|
||||||
|
_inMemoryFileStorage?.Clear(expiryDuration); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Removed items based on uri list passed |
||||||
|
/// </summary> |
||||||
|
/// <param name="uriForCachedItems">Enumerable uri list</param> |
||||||
|
/// <returns>awaitable Task</returns> |
||||||
|
public async Task RemoveAsync(IEnumerable<Uri> uriForCachedItems) |
||||||
|
{ |
||||||
|
if (uriForCachedItems == null || !uriForCachedItems.Any()) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var folder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
var files = Directory.EnumerateFiles(folder!); |
||||||
|
var filesToDelete = new List<string>(); |
||||||
|
var keys = new List<string>(); |
||||||
|
|
||||||
|
Dictionary<string, string> hashDictionary = new Dictionary<string, string>(); |
||||||
|
|
||||||
|
foreach (var file in files) |
||||||
|
{ |
||||||
|
hashDictionary.Add(Path.GetFileName(file), file); |
||||||
|
} |
||||||
|
|
||||||
|
foreach (var uri in uriForCachedItems) |
||||||
|
{ |
||||||
|
string fileName = GetCacheFileName(uri); |
||||||
|
if (hashDictionary.TryGetValue(fileName, out var file)) |
||||||
|
{ |
||||||
|
filesToDelete.Add(file); |
||||||
|
keys.Add(fileName); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
await InternalClearAsync(filesToDelete).ConfigureAwait(false); |
||||||
|
|
||||||
|
_inMemoryFileStorage?.Remove(keys); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Assures that item represented by Uri is cached. |
||||||
|
/// </summary> |
||||||
|
/// <param name="uri">Uri of the item</param> |
||||||
|
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be cached</param> |
||||||
|
/// <param name="storeToMemoryCache">Indicates if item should be loaded into the in-memory storage</param> |
||||||
|
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param> |
||||||
|
/// <returns>Awaitable Task</returns> |
||||||
|
public Task PreCacheAsync( |
||||||
|
Uri uri, |
||||||
|
bool throwOnError = false, |
||||||
|
bool storeToMemoryCache = false, |
||||||
|
CancellationToken cancellationToken = default(CancellationToken) |
||||||
|
) |
||||||
|
{ |
||||||
|
return GetItemAsync(uri, throwOnError, !storeToMemoryCache, cancellationToken); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Retrieves item represented by Uri from the cache. If the item is not found in the cache, it will try to downloaded and saved before returning it to the caller. |
||||||
|
/// </summary> |
||||||
|
/// <param name="uri">Uri of the item.</param> |
||||||
|
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be found / downloaded.</param> |
||||||
|
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param> |
||||||
|
/// <returns>an instance of Generic type</returns> |
||||||
|
public Task<T?> GetFromCacheAsync( |
||||||
|
Uri uri, |
||||||
|
bool throwOnError = false, |
||||||
|
CancellationToken cancellationToken = default(CancellationToken) |
||||||
|
) |
||||||
|
{ |
||||||
|
return GetItemAsync(uri, throwOnError, false, cancellationToken); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets the string containing cached item for given Uri |
||||||
|
/// </summary> |
||||||
|
/// <param name="uri">Uri of the item.</param> |
||||||
|
/// <returns>a string</returns> |
||||||
|
public async Task<string> GetFileFromCacheAsync(Uri uri) |
||||||
|
{ |
||||||
|
var folder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
|
||||||
|
return Path.Combine(folder!, GetCacheFileName(uri)); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Retrieves item represented by Uri from the in-memory cache if it exists and is not out of date. If item is not found or is out of date, default instance of the generic type is returned. |
||||||
|
/// </summary> |
||||||
|
/// <param name="uri">Uri of the item.</param> |
||||||
|
/// <returns>an instance of Generic type</returns> |
||||||
|
public T? GetFromMemoryCache(Uri uri) |
||||||
|
{ |
||||||
|
T? instance = default(T); |
||||||
|
|
||||||
|
string fileName = GetCacheFileName(uri); |
||||||
|
|
||||||
|
if (_inMemoryFileStorage?.MaxItemCount > 0) |
||||||
|
{ |
||||||
|
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration); |
||||||
|
if (msi != null) |
||||||
|
{ |
||||||
|
instance = msi.Item; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Cache specific hooks to process items from HTTP response |
||||||
|
/// </summary> |
||||||
|
/// <param name="stream">input stream</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
protected abstract Task<T> ConvertFromAsync(Stream stream); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Cache specific hooks to process items from HTTP response |
||||||
|
/// </summary> |
||||||
|
/// <param name="baseFile">storage file</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
protected abstract Task<T> ConvertFromAsync(string baseFile); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Override-able method that checks whether file is valid or not. |
||||||
|
/// </summary> |
||||||
|
/// <param name="file">storage file</param> |
||||||
|
/// <param name="duration">cache duration</param> |
||||||
|
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param> |
||||||
|
/// <returns>bool indicate whether file has expired or not</returns> |
||||||
|
protected virtual async Task<bool> IsFileOutOfDateAsync( |
||||||
|
string file, |
||||||
|
TimeSpan duration, |
||||||
|
bool treatNullFileAsOutOfDate = true |
||||||
|
) |
||||||
|
{ |
||||||
|
if (file == null) |
||||||
|
{ |
||||||
|
return treatNullFileAsOutOfDate; |
||||||
|
} |
||||||
|
|
||||||
|
var info = new FileInfo(file); |
||||||
|
|
||||||
|
return info.Length == 0 || DateTime.Now.Subtract(info.LastWriteTime) > duration; |
||||||
|
} |
||||||
|
|
||||||
|
private static string GetCacheFileName(Uri uri) |
||||||
|
{ |
||||||
|
return CreateHash64(uri.ToString()).ToString(); |
||||||
|
} |
||||||
|
|
||||||
|
private static ulong CreateHash64(string str) |
||||||
|
{ |
||||||
|
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(str); |
||||||
|
|
||||||
|
ulong value = (ulong)utf8.Length; |
||||||
|
for (int n = 0; n < utf8.Length; n++) |
||||||
|
{ |
||||||
|
value += (ulong)utf8[n] << ((n * 5) % 56); |
||||||
|
} |
||||||
|
|
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<T?> GetItemAsync( |
||||||
|
Uri uri, |
||||||
|
bool throwOnError, |
||||||
|
bool preCacheOnly, |
||||||
|
CancellationToken cancellationToken |
||||||
|
) |
||||||
|
{ |
||||||
|
T? instance = default(T); |
||||||
|
|
||||||
|
string fileName = GetCacheFileName(uri); |
||||||
|
_concurrentTasks.TryGetValue(fileName, out var request); |
||||||
|
|
||||||
|
// if similar request exists check if it was preCacheOnly and validate that current request isn't preCacheOnly |
||||||
|
if (request != null && request.EnsureCachedCopy && !preCacheOnly) |
||||||
|
{ |
||||||
|
if (request.Task != null) |
||||||
|
await request.Task.ConfigureAwait(false); |
||||||
|
request = null; |
||||||
|
} |
||||||
|
|
||||||
|
if (request == null) |
||||||
|
{ |
||||||
|
request = new ConcurrentRequest() |
||||||
|
{ |
||||||
|
Task = GetFromCacheOrDownloadAsync(uri, fileName, preCacheOnly, cancellationToken), |
||||||
|
EnsureCachedCopy = preCacheOnly |
||||||
|
}; |
||||||
|
|
||||||
|
_concurrentTasks[fileName] = request; |
||||||
|
} |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
if (request.Task != null) |
||||||
|
instance = await request.Task.ConfigureAwait(false); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
System.Diagnostics.Debug.WriteLine(ex.Message); |
||||||
|
if (throwOnError) |
||||||
|
{ |
||||||
|
throw; |
||||||
|
} |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
_concurrentTasks.TryRemove(fileName, out _); |
||||||
|
} |
||||||
|
|
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<T?> GetFromCacheOrDownloadAsync( |
||||||
|
Uri uri, |
||||||
|
string fileName, |
||||||
|
bool preCacheOnly, |
||||||
|
CancellationToken cancellationToken |
||||||
|
) |
||||||
|
{ |
||||||
|
T? instance = default(T); |
||||||
|
|
||||||
|
if (_inMemoryFileStorage?.MaxItemCount > 0) |
||||||
|
{ |
||||||
|
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration); |
||||||
|
if (msi != null) |
||||||
|
{ |
||||||
|
instance = msi.Item; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (instance != null) |
||||||
|
{ |
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
var folder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
var baseFile = Path.Combine(folder!, fileName); |
||||||
|
|
||||||
|
bool downloadDataFile = |
||||||
|
!File.Exists(baseFile) |
||||||
|
|| await IsFileOutOfDateAsync(baseFile, CacheDuration).ConfigureAwait(false); |
||||||
|
|
||||||
|
if (!File.Exists(baseFile)) |
||||||
|
{ |
||||||
|
File.Create(baseFile).Dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
if (downloadDataFile) |
||||||
|
{ |
||||||
|
uint retries = 0; |
||||||
|
try |
||||||
|
{ |
||||||
|
while (retries < RetryCount) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
instance = await DownloadFileAsync(uri, baseFile, preCacheOnly, cancellationToken) |
||||||
|
.ConfigureAwait(false); |
||||||
|
|
||||||
|
if (instance != null) |
||||||
|
{ |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
catch (FileNotFoundException) { } |
||||||
|
|
||||||
|
retries++; |
||||||
|
} |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
File.Delete(baseFile); |
||||||
|
throw; // re-throwing the exception changes the stack trace. just throw |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (EqualityComparer<T>.Default.Equals(instance, default(T)) && !preCacheOnly) |
||||||
|
{ |
||||||
|
instance = await ConvertFromAsync(baseFile).ConfigureAwait(false); |
||||||
|
|
||||||
|
if (_inMemoryFileStorage?.MaxItemCount > 0) |
||||||
|
{ |
||||||
|
var properties = new FileInfo(baseFile); |
||||||
|
|
||||||
|
var msi = new InMemoryStorageItem<T>(fileName, properties.LastWriteTime, instance); |
||||||
|
_inMemoryFileStorage?.SetItem(msi); |
||||||
|
} |
||||||
|
} |
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<T?> DownloadFileAsync( |
||||||
|
Uri uri, |
||||||
|
string baseFile, |
||||||
|
bool preCacheOnly, |
||||||
|
CancellationToken cancellationToken |
||||||
|
) |
||||||
|
{ |
||||||
|
T? instance = default(T); |
||||||
|
|
||||||
|
using (MemoryStream ms = new MemoryStream()) |
||||||
|
{ |
||||||
|
using (var stream = await HttpClient.GetStreamAsync(uri)) |
||||||
|
{ |
||||||
|
stream.CopyTo(ms); |
||||||
|
ms.Flush(); |
||||||
|
|
||||||
|
ms.Position = 0; |
||||||
|
|
||||||
|
using (var fs = File.Open(baseFile, FileMode.OpenOrCreate, FileAccess.Write)) |
||||||
|
{ |
||||||
|
ms.CopyTo(fs); |
||||||
|
|
||||||
|
fs.Flush(); |
||||||
|
|
||||||
|
ms.Position = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// if its pre-cache we aren't looking to load items in memory |
||||||
|
if (!preCacheOnly) |
||||||
|
{ |
||||||
|
instance = await ConvertFromAsync(ms).ConfigureAwait(false); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task InternalClearAsync(IEnumerable<string?> files) |
||||||
|
{ |
||||||
|
foreach (var file in files) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
File.Delete(file!); |
||||||
|
} |
||||||
|
catch |
||||||
|
{ |
||||||
|
// Just ignore errors for now} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Initializes with default values if user has not initialized explicitly |
||||||
|
/// </summary> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
private async Task ForceInitialiseAsync() |
||||||
|
{ |
||||||
|
if (_cacheFolder != null) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
await _cacheFolderSemaphore.WaitAsync().ConfigureAwait(false); |
||||||
|
|
||||||
|
_inMemoryFileStorage = new InMemoryStorage<T>(); |
||||||
|
|
||||||
|
if (_baseFolder == null) |
||||||
|
{ |
||||||
|
_baseFolder = Path.GetTempPath(); |
||||||
|
} |
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_cacheFolderName)) |
||||||
|
{ |
||||||
|
_cacheFolderName = GetType().Name; |
||||||
|
} |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
_cacheFolder = Path.Combine(_baseFolder, _cacheFolderName); |
||||||
|
Directory.CreateDirectory(_cacheFolder); |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
_cacheFolderSemaphore.Release(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<string?> GetCacheFolderAsync() |
||||||
|
{ |
||||||
|
if (_cacheFolder == null) |
||||||
|
{ |
||||||
|
await ForceInitialiseAsync().ConfigureAwait(false); |
||||||
|
} |
||||||
|
|
||||||
|
return _cacheFolder; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
using System; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
public class CacheOptions |
||||||
|
{ |
||||||
|
private static CacheOptions? _cacheOptions; |
||||||
|
|
||||||
|
public static CacheOptions Default => _cacheOptions ??= new CacheOptions(); |
||||||
|
|
||||||
|
public static void SetDefault(CacheOptions defaultCacheOptions) |
||||||
|
{ |
||||||
|
_cacheOptions = defaultCacheOptions; |
||||||
|
} |
||||||
|
|
||||||
|
public string? BaseCachePath { get; set; } |
||||||
|
public TimeSpan? CacheDuration { get; set; } |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
using System.IO; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Provides methods and tools to cache files in a folder |
||||||
|
/// </summary> |
||||||
|
internal class FileCache : CacheBase<string> |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Private singleton field. |
||||||
|
/// </summary> |
||||||
|
private static FileCache? _instance; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets public singleton property. |
||||||
|
/// </summary> |
||||||
|
public static FileCache Instance => _instance ?? (_instance = new FileCache()); |
||||||
|
|
||||||
|
protected override Task<string> ConvertFromAsync(Stream stream) |
||||||
|
{ |
||||||
|
// nothing to do in this instance; |
||||||
|
return Task.FromResult<string>(""); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Returns a cached path |
||||||
|
/// </summary> |
||||||
|
/// <param name="baseFile">storage file</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
protected override Task<string> ConvertFromAsync(string baseFile) |
||||||
|
{ |
||||||
|
return Task.FromResult(baseFile); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using Avalonia.Media.Imaging; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Provides methods and tools to cache images in a folder |
||||||
|
/// </summary> |
||||||
|
internal class ImageCache : CacheBase<Bitmap> |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Private singleton field. |
||||||
|
/// </summary> |
||||||
|
[ThreadStatic] |
||||||
|
private static ImageCache? _instance; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets public singleton property. |
||||||
|
/// </summary> |
||||||
|
public static ImageCache Instance => _instance ?? (_instance = new ImageCache()); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Creates a bitmap from a stream |
||||||
|
/// </summary> |
||||||
|
/// <param name="stream">input stream</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
protected override async Task<Bitmap> ConvertFromAsync(Stream stream) |
||||||
|
{ |
||||||
|
if (stream.Length == 0) |
||||||
|
{ |
||||||
|
throw new FileNotFoundException(); |
||||||
|
} |
||||||
|
|
||||||
|
return new Bitmap(stream); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Creates a bitmap from a cached file |
||||||
|
/// </summary> |
||||||
|
/// <param name="baseFile">file</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
protected override async Task<Bitmap> ConvertFromAsync(string baseFile) |
||||||
|
{ |
||||||
|
using (var stream = File.OpenRead(baseFile)) |
||||||
|
{ |
||||||
|
return await ConvertFromAsync(stream).ConfigureAwait(false); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Checks whether file is valid or not. |
||||||
|
/// </summary> |
||||||
|
/// <param name="file">file</param> |
||||||
|
/// <param name="duration">cache duration</param> |
||||||
|
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param> |
||||||
|
/// <returns>bool indicate whether file has expired or not</returns> |
||||||
|
protected override async Task<bool> IsFileOutOfDateAsync( |
||||||
|
string file, |
||||||
|
TimeSpan duration, |
||||||
|
bool treatNullFileAsOutOfDate = true |
||||||
|
) |
||||||
|
{ |
||||||
|
if (file == null) |
||||||
|
{ |
||||||
|
return treatNullFileAsOutOfDate; |
||||||
|
} |
||||||
|
|
||||||
|
var fileInfo = new FileInfo(file); |
||||||
|
|
||||||
|
return fileInfo.Length == 0 |
||||||
|
|| DateTime.Now.Subtract(File.GetLastAccessTime(file)) > duration |
||||||
|
|| DateTime.Now.Subtract(File.GetLastWriteTime(file)) > duration; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,156 @@ |
|||||||
|
// Licensed to the .NET Foundation under one or more agreements. |
||||||
|
// The .NET Foundation licenses this file to you under the MIT license. |
||||||
|
// See the LICENSE file in the project root for more information. |
||||||
|
|
||||||
|
using System; |
||||||
|
using System.Collections.Concurrent; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using Avalonia.Labs.Controls.Cache; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Generic in-memory storage of items |
||||||
|
/// </summary> |
||||||
|
/// <typeparam name="T">T defines the type of item stored</typeparam> |
||||||
|
public class InMemoryStorage<T> |
||||||
|
{ |
||||||
|
private int _maxItemCount; |
||||||
|
private ConcurrentDictionary<string, InMemoryStorageItem<T>> _inMemoryStorage = |
||||||
|
new ConcurrentDictionary<string, InMemoryStorageItem<T>>(); |
||||||
|
private object _settingMaxItemCountLocker = new object(); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the maximum count of Items that can be stored in this InMemoryStorage instance. |
||||||
|
/// </summary> |
||||||
|
public int MaxItemCount |
||||||
|
{ |
||||||
|
get { return _maxItemCount; } |
||||||
|
set |
||||||
|
{ |
||||||
|
if (_maxItemCount == value) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
_maxItemCount = value; |
||||||
|
|
||||||
|
lock (_settingMaxItemCountLocker) |
||||||
|
{ |
||||||
|
EnsureStorageBounds(value); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Clears all items stored in memory |
||||||
|
/// </summary> |
||||||
|
public void Clear() |
||||||
|
{ |
||||||
|
_inMemoryStorage.Clear(); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Clears items stored in memory based on duration passed |
||||||
|
/// </summary> |
||||||
|
/// <param name="duration">TimeSpan to identify expired items</param> |
||||||
|
public void Clear(TimeSpan duration) |
||||||
|
{ |
||||||
|
var expirationDate = DateTime.Now.Subtract(duration); |
||||||
|
|
||||||
|
var itemsToRemove = _inMemoryStorage |
||||||
|
.Where(kvp => kvp.Value.LastUpdated <= expirationDate) |
||||||
|
.Select(kvp => kvp.Key); |
||||||
|
|
||||||
|
if (itemsToRemove.Any()) |
||||||
|
{ |
||||||
|
Remove(itemsToRemove); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Remove items based on provided keys |
||||||
|
/// </summary> |
||||||
|
/// <param name="keys">identified of the in-memory storage item</param> |
||||||
|
public void Remove(IEnumerable<string> keys) |
||||||
|
{ |
||||||
|
foreach (var key in keys) |
||||||
|
{ |
||||||
|
if (string.IsNullOrWhiteSpace(key)) |
||||||
|
{ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
_inMemoryStorage.TryRemove(key, out _); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Add new item to in-memory storage |
||||||
|
/// </summary> |
||||||
|
/// <param name="item">item to be stored</param> |
||||||
|
public void SetItem(InMemoryStorageItem<T> item) |
||||||
|
{ |
||||||
|
if (MaxItemCount == 0) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
_inMemoryStorage[item.Id] = item; |
||||||
|
|
||||||
|
// ensure max limit is maintained. trim older entries first |
||||||
|
if (_inMemoryStorage.Count > MaxItemCount) |
||||||
|
{ |
||||||
|
var itemsToRemove = _inMemoryStorage |
||||||
|
.OrderBy(kvp => kvp.Value.Created) |
||||||
|
.Take(_inMemoryStorage.Count - MaxItemCount) |
||||||
|
.Select(kvp => kvp.Key); |
||||||
|
Remove(itemsToRemove); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Get item from in-memory storage as long as it has not ex |
||||||
|
/// </summary> |
||||||
|
/// <param name="id">id of the in-memory storage item</param> |
||||||
|
/// <param name="duration">timespan denoting expiration</param> |
||||||
|
/// <returns>Valid item if not out of date or return null if out of date or item does not exist</returns> |
||||||
|
public InMemoryStorageItem<T>? GetItem(string id, TimeSpan duration) |
||||||
|
{ |
||||||
|
if (!_inMemoryStorage.TryGetValue(id, out var tempItem)) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
var expirationDate = DateTime.Now.Subtract(duration); |
||||||
|
|
||||||
|
if (tempItem.LastUpdated > expirationDate) |
||||||
|
{ |
||||||
|
return tempItem; |
||||||
|
} |
||||||
|
|
||||||
|
_inMemoryStorage.TryRemove(id, out _); |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private void EnsureStorageBounds(int maxCount) |
||||||
|
{ |
||||||
|
if (_inMemoryStorage.Count == 0) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (maxCount == 0) |
||||||
|
{ |
||||||
|
_inMemoryStorage.Clear(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (_inMemoryStorage.Count > maxCount) |
||||||
|
{ |
||||||
|
Remove(_inMemoryStorage.Keys.Take(_inMemoryStorage.Count - maxCount)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
// Licensed to the .NET Foundation under one or more agreements. |
||||||
|
// The .NET Foundation licenses this file to you under the MIT license. |
||||||
|
// See the LICENSE file in the project root for more information. |
||||||
|
|
||||||
|
using System; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Generic InMemoryStorageItem holds items for InMemoryStorage. |
||||||
|
/// </summary> |
||||||
|
/// <typeparam name="T">Type is set by consuming cache</typeparam> |
||||||
|
public class InMemoryStorageItem<T> |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Gets the item identifier |
||||||
|
/// </summary> |
||||||
|
public string Id { get; private set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets the item created timestamp. |
||||||
|
/// </summary> |
||||||
|
public DateTime Created { get; private set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets the item last updated timestamp. |
||||||
|
/// </summary> |
||||||
|
public DateTime LastUpdated { get; private set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets the item being stored. |
||||||
|
/// </summary> |
||||||
|
public T Item { get; private set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Initializes a new instance of the <see cref="InMemoryStorageItem{T}"/> class. |
||||||
|
/// Constructor for InMemoryStorageItem |
||||||
|
/// </summary> |
||||||
|
/// <param name="id">uniquely identifies the item</param> |
||||||
|
/// <param name="lastUpdated">last updated timestamp</param> |
||||||
|
/// <param name="item">the item being stored</param> |
||||||
|
public InMemoryStorageItem(string id, DateTime lastUpdated, T item) |
||||||
|
{ |
||||||
|
Id = id; |
||||||
|
LastUpdated = lastUpdated; |
||||||
|
Item = item; |
||||||
|
Created = DateTime.Now; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2023 AvaloniaUI |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
@ -0,0 +1,48 @@ |
|||||||
|
<ResourceDictionary |
||||||
|
xmlns="https://github.com/avaloniaui" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs"> |
||||||
|
<Design.PreviewWith> |
||||||
|
<Border Width="200" |
||||||
|
Height="200"> |
||||||
|
|
||||||
|
</Border> |
||||||
|
</Design.PreviewWith> |
||||||
|
|
||||||
|
<ControlTheme x:Key="{x:Type vendorLabs:BetterAsyncImage}" |
||||||
|
TargetType="vendorLabs:BetterAsyncImage"> |
||||||
|
<Setter Property="Background" Value="Transparent" /> |
||||||
|
<Setter Property="IsTabStop" Value="False" /> |
||||||
|
<Setter Property="Template"> |
||||||
|
<ControlTemplate> |
||||||
|
<Border Margin="0" |
||||||
|
Padding="0" |
||||||
|
ClipToBounds="True" |
||||||
|
Background="{TemplateBinding Background}" |
||||||
|
BorderBrush="{TemplateBinding BorderBrush}" |
||||||
|
BorderThickness="{TemplateBinding BorderThickness}" |
||||||
|
CornerRadius="{TemplateBinding CornerRadius}"> |
||||||
|
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> |
||||||
|
<Image Name="PART_PlaceholderImage" |
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalAlignment}" |
||||||
|
VerticalAlignment="{TemplateBinding VerticalAlignment}" |
||||||
|
Source="{TemplateBinding PlaceholderSource}" |
||||||
|
Stretch="{TemplateBinding PlaceholderStretch}"/> |
||||||
|
<Image Name="PART_Image" |
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalAlignment}" |
||||||
|
VerticalAlignment="{TemplateBinding VerticalAlignment}" |
||||||
|
Stretch="{TemplateBinding Stretch}"/> |
||||||
|
</Grid> |
||||||
|
</Border> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter> |
||||||
|
<Style Selector="^[State=Failed] /template/ Image#PART_Image"> |
||||||
|
<Setter Property="Opacity" |
||||||
|
Value="0.0" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^[State=Failed] /template/ Image#PART_PlaceholderImage"> |
||||||
|
<Setter Property="Opacity" |
||||||
|
Value="1.0" /> |
||||||
|
</Style> |
||||||
|
</ControlTheme> |
||||||
|
</ResourceDictionary> |
Loading…
Reference in new issue