mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
optimize Cache performance
This commit is contained in:
@@ -6,6 +6,7 @@ using Microsoft.UI.Xaml.Media;
|
|||||||
using Microsoft.UI.Xaml.Media.Imaging;
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
using Snap.Hutao.Core.Caching;
|
using Snap.Hutao.Core.Caching;
|
||||||
using Snap.Hutao.Extension;
|
using Snap.Hutao.Extension;
|
||||||
|
using Windows.Storage;
|
||||||
|
|
||||||
namespace Snap.Hutao.Control.Image;
|
namespace Snap.Hutao.Control.Image;
|
||||||
|
|
||||||
@@ -14,15 +15,11 @@ namespace Snap.Hutao.Control.Image;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CachedImage : ImageEx
|
public class CachedImage : ImageEx
|
||||||
{
|
{
|
||||||
private readonly IImageCache imageCache;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个新的缓存图像
|
/// 构造一个新的缓存图像
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CachedImage()
|
public CachedImage()
|
||||||
{
|
{
|
||||||
imageCache = Ioc.Default.GetRequiredService<IImageCache>();
|
|
||||||
|
|
||||||
IsCacheEnabled = true;
|
IsCacheEnabled = true;
|
||||||
EnableLazyLoading = true;
|
EnableLazyLoading = true;
|
||||||
}
|
}
|
||||||
@@ -30,10 +27,15 @@ public class CachedImage : ImageEx
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override async Task<ImageSource> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
protected override async Task<ImageSource> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||||
{
|
{
|
||||||
BitmapImage? image;
|
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
image = await imageCache.GetFromCacheAsync(imageUri, true);
|
StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri);
|
||||||
|
|
||||||
|
// check token state to determine whether the operation should be canceled.
|
||||||
|
Must.TryThrowOnCanceled(token, "Image source has changed.");
|
||||||
|
return new BitmapImage(new(file.Path));
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
@@ -43,18 +45,8 @@ public class CachedImage : ImageEx
|
|||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// maybe the image is corrupted, remove it.
|
// maybe the image is corrupted, remove it.
|
||||||
await imageCache.RemoveAsync(imageUri.Enumerate());
|
await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check token state to determine whether the operation should be canceled.
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
throw new TaskCanceledException("Image source has changed.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Must.NotNull(image!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control
|
|||||||
{
|
{
|
||||||
Ioc.Default
|
Ioc.Default
|
||||||
.GetRequiredService<IInfoBarService>()
|
.GetRequiredService<IInfoBarService>()
|
||||||
.Error(exception, "应用渐变背景时发生异常");
|
.Error(exception, "应用渐变图像时发生异常");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<StorageFile?> GetCachedFileAsync(string url)
|
private static Task<StorageFile?> GetCachedFileAsync(string url)
|
||||||
|
|||||||
149
src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs
Normal file
149
src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using CommunityToolkit.WinUI.UI.Animations;
|
||||||
|
using Microsoft.Graphics.Canvas.Effects;
|
||||||
|
using Microsoft.UI;
|
||||||
|
using Microsoft.UI.Composition;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Hosting;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using Snap.Hutao.Core;
|
||||||
|
using Snap.Hutao.Core.Caching;
|
||||||
|
using Snap.Hutao.Core.Threading;
|
||||||
|
using Snap.Hutao.Extension;
|
||||||
|
using Snap.Hutao.Service.Abstraction;
|
||||||
|
using Windows.Graphics.Imaging;
|
||||||
|
using Windows.Storage;
|
||||||
|
using Windows.Storage.Streams;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Image;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持单色的图像
|
||||||
|
/// </summary>
|
||||||
|
public class MonoChrome : Microsoft.UI.Xaml.Controls.Control
|
||||||
|
{
|
||||||
|
private static readonly DependencyProperty SourceProperty = Property<MonoChrome>.Depend(nameof(Source), string.Empty, OnSourceChanged);
|
||||||
|
private static readonly ConcurrentCancellationTokenSource<MonoChrome> LoadingTokenSource = new();
|
||||||
|
|
||||||
|
private SpriteVisual? spriteVisual;
|
||||||
|
private CompositionColorBrush? backgroundBrush;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造一个新的单色图像
|
||||||
|
/// </summary>
|
||||||
|
public MonoChrome()
|
||||||
|
{
|
||||||
|
SizeChanged += OnSizeChanged;
|
||||||
|
ActualThemeChanged += OnActualThemeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 源
|
||||||
|
/// </summary>
|
||||||
|
public string Source
|
||||||
|
{
|
||||||
|
get => (string)GetValue(SourceProperty);
|
||||||
|
set => SetValue(SourceProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
|
||||||
|
{
|
||||||
|
MonoChrome monoChrome = (MonoChrome)sender;
|
||||||
|
string url = (string)arg.NewValue;
|
||||||
|
|
||||||
|
ILogger<MonoChrome> logger = Ioc.Default.GetRequiredService<ILogger<MonoChrome>>();
|
||||||
|
monoChrome.ApplyImageAsync(url, LoadingTokenSource.Register(monoChrome)).SafeForget(logger, OnApplyImageFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnApplyImageFailed(Exception exception)
|
||||||
|
{
|
||||||
|
Ioc.Default
|
||||||
|
.GetRequiredService<IInfoBarService>()
|
||||||
|
.Error(exception, "应用单色背景时发生异常");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
|
||||||
|
{
|
||||||
|
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
|
||||||
|
{
|
||||||
|
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token);
|
||||||
|
return LoadedImageSurface.StartLoadFromStream(imageStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<StorageFile?> GetCachedFileAsync(string url)
|
||||||
|
{
|
||||||
|
return Ioc.Default.GetRequiredService<IImageCache>().GetFileFromCacheAsync(new(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.NewSize != e.PreviousSize && spriteVisual is not null)
|
||||||
|
{
|
||||||
|
UpdateVisual(spriteVisual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||||
|
{
|
||||||
|
if (backgroundBrush != null)
|
||||||
|
{
|
||||||
|
SetBackgroundColor(backgroundBrush);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateVisual(SpriteVisual spriteVisual)
|
||||||
|
{
|
||||||
|
if (spriteVisual is not null)
|
||||||
|
{
|
||||||
|
spriteVisual.Size = ActualSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetBackgroundColor(CompositionColorBrush backgroundBrush)
|
||||||
|
{
|
||||||
|
ApplicationTheme theme = ActualTheme switch
|
||||||
|
{
|
||||||
|
ElementTheme.Light => ApplicationTheme.Light,
|
||||||
|
ElementTheme.Dark => ApplicationTheme.Dark,
|
||||||
|
_ => App.Current.RequestedTheme,
|
||||||
|
};
|
||||||
|
|
||||||
|
backgroundBrush.Color = theme switch
|
||||||
|
{
|
||||||
|
ApplicationTheme.Light => Colors.Black,
|
||||||
|
ApplicationTheme.Dark => Colors.White,
|
||||||
|
_ => throw Must.NeverHappen(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyImageAsync(string url, CancellationToken token)
|
||||||
|
{
|
||||||
|
await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token);
|
||||||
|
|
||||||
|
StorageFile? storageFile = Must.NotNull((await GetCachedFileAsync(url))!);
|
||||||
|
|
||||||
|
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
|
||||||
|
|
||||||
|
LoadedImageSurface imageSurface = await LoadImageSurfaceAsync(storageFile, token);
|
||||||
|
|
||||||
|
CompositionColorBrush blackLayerBursh = compositor.CreateColorBrush(Colors.Black);
|
||||||
|
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.Uniform, vRatio: 0f);
|
||||||
|
|
||||||
|
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBursh, imageSurfaceBrush, BlendEffectMode.Overlay);
|
||||||
|
CompositionEffectBrush opacityBrush = compositor.CompositeLuminanceToAlphaEffectBrush(overlayBrush);
|
||||||
|
|
||||||
|
backgroundBrush = compositor.CreateColorBrush();
|
||||||
|
SetBackgroundColor(backgroundBrush);
|
||||||
|
CompositionEffectBrush alphaMaskEffectBrush = compositor.CompositeAlphaMaskEffectBrush(backgroundBrush, opacityBrush);
|
||||||
|
|
||||||
|
spriteVisual = compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
|
||||||
|
UpdateVisual(spriteVisual);
|
||||||
|
|
||||||
|
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
|
||||||
|
|
||||||
|
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using Snap.Hutao.Core.Logging;
|
using Snap.Hutao.Core.Logging;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -23,7 +22,6 @@ public abstract class CacheBase<T>
|
|||||||
where T : class
|
where T : class
|
||||||
{
|
{
|
||||||
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
|
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
|
||||||
private readonly ConcurrentDictionary<string, Task<T?>> concurrentTasks = new();
|
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
private readonly HttpClient httpClient;
|
private readonly HttpClient httpClient;
|
||||||
|
|
||||||
@@ -48,12 +46,12 @@ public abstract class CacheBase<T>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the life duration of every cache entry.
|
/// Gets or sets the life duration of every cache entry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan CacheDuration { get; set; }
|
public TimeSpan CacheDuration { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the number of retries trying to ensure the file is cached.
|
/// Gets or sets the number of retries trying to ensure the file is cached.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint RetryCount { get; set; }
|
public uint RetryCount { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears all files in the cache
|
/// Clears all files in the cache
|
||||||
@@ -76,12 +74,12 @@ public abstract class CacheBase<T>
|
|||||||
{
|
{
|
||||||
TimeSpan expiryDuration = duration ?? CacheDuration;
|
TimeSpan expiryDuration = duration ?? CacheDuration;
|
||||||
|
|
||||||
StorageFolder? folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
||||||
IReadOnlyList<StorageFile>? files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
|
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
|
||||||
|
|
||||||
List<StorageFile>? filesToDelete = new();
|
List<StorageFile> filesToDelete = new();
|
||||||
|
|
||||||
foreach (StorageFile? file in files)
|
foreach (StorageFile file in files)
|
||||||
{
|
{
|
||||||
if (file == null)
|
if (file == null)
|
||||||
{
|
{
|
||||||
@@ -135,24 +133,14 @@ public abstract class CacheBase<T>
|
|||||||
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
|
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// <returns>an instance of Generic type</returns>
|
|
||||||
public Task<T?> GetFromCacheAsync(Uri uri, bool throwOnError = false)
|
|
||||||
{
|
|
||||||
return GetItemAsync(uri, throwOnError);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the StorageFile containing cached item for given Uri
|
/// Gets the StorageFile containing cached item for given Uri
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uri">Uri of the item.</param>
|
/// <param name="uri">Uri of the item.</param>
|
||||||
/// <returns>a StorageFile</returns>
|
/// <returns>a StorageFile</returns>
|
||||||
public async Task<StorageFile?> GetFileFromCacheAsync(Uri uri)
|
public async Task<StorageFile> GetFileFromCacheAsync(Uri uri)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation(EventIds.FileCaching, "Begin caching for [{uri}]", uri);
|
||||||
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
string fileName = GetCacheFileName(uri);
|
string fileName = GetCacheFileName(uri);
|
||||||
@@ -161,28 +149,14 @@ public abstract class CacheBase<T>
|
|||||||
|
|
||||||
if (item == null)
|
if (item == null)
|
||||||
{
|
{
|
||||||
StorageFile? baseFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
|
StorageFile baseFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
|
||||||
await DownloadFileAsync(uri, baseFile).ConfigureAwait(false);
|
await DownloadFileAsync(uri, baseFile).ConfigureAwait(false);
|
||||||
item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
|
item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return item as StorageFile;
|
return Must.NotNull((item as StorageFile)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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> InitializeTypeAsync(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> InitializeTypeAsync(StorageFile baseFile);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Override-able method that checks whether file is valid or not.
|
/// Override-able method that checks whether file is valid or not.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -220,121 +194,16 @@ public abstract class CacheBase<T>
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<T?> GetItemAsync(Uri uri, bool throwOnError)
|
private async Task DownloadFileAsync(Uri uri, StorageFile baseFile)
|
||||||
{
|
{
|
||||||
T? instance = default(T);
|
using (Stream httpStream = await httpClient.GetStreamAsync(uri))
|
||||||
|
|
||||||
string fileName = GetCacheFileName(uri);
|
|
||||||
concurrentTasks.TryGetValue(fileName, out Task<T?>? request);
|
|
||||||
|
|
||||||
// complete previous task first
|
|
||||||
if (request != null)
|
|
||||||
{
|
{
|
||||||
await request.ConfigureAwait(false);
|
using (Stream fs = await baseFile.OpenStreamForWriteAsync())
|
||||||
request = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request == null)
|
|
||||||
{
|
|
||||||
request = GetFromCacheOrDownloadAsync(uri, fileName);
|
|
||||||
|
|
||||||
concurrentTasks[fileName] = request;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
instance = await request.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(EventIds.CacheException, ex, "Exception happened when caching.");
|
|
||||||
if (throwOnError)
|
|
||||||
{
|
{
|
||||||
throw;
|
await httpStream.CopyToAsync(fs);
|
||||||
|
await fs.FlushAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
concurrentTasks.TryRemove(fileName, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<T?> GetFromCacheOrDownloadAsync(Uri uri, string fileName)
|
|
||||||
{
|
|
||||||
T? instance = default;
|
|
||||||
|
|
||||||
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
|
||||||
StorageFile? baseFile = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false) as StorageFile;
|
|
||||||
|
|
||||||
bool downloadDataFile = baseFile == null || await IsFileOutOfDateAsync(baseFile, CacheDuration).ConfigureAwait(false);
|
|
||||||
baseFile ??= await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (downloadDataFile)
|
|
||||||
{
|
|
||||||
uint retries = 0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (retries < RetryCount)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
instance = await DownloadFileAsync(uri, baseFile).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (instance != null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
retries++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
await baseFile.DeleteAsync().AsTask().ConfigureAwait(false);
|
|
||||||
throw; // re-throwing the exception changes the stack trace. just throw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (EqualityComparer<T>.Default.Equals(instance, default(T)))
|
|
||||||
{
|
|
||||||
instance = await InitializeTypeAsync(baseFile).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<T?> DownloadFileAsync(Uri uri, StorageFile baseFile)
|
|
||||||
{
|
|
||||||
T? instance = default;
|
|
||||||
|
|
||||||
using (MemoryStream memory = new())
|
|
||||||
{
|
|
||||||
using (Stream httpStream = await httpClient.GetStreamAsync(uri))
|
|
||||||
{
|
|
||||||
await httpStream.CopyToAsync(memory);
|
|
||||||
await memory.FlushAsync();
|
|
||||||
|
|
||||||
memory.Position = 0;
|
|
||||||
|
|
||||||
using (Stream fs = await baseFile.OpenStreamForWriteAsync())
|
|
||||||
{
|
|
||||||
await memory.CopyToAsync(fs);
|
|
||||||
await fs.FlushAsync();
|
|
||||||
|
|
||||||
memory.Position = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
instance = await InitializeTypeAsync(memory).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("", "CA1822")]
|
[SuppressMessage("", "CA1822")]
|
||||||
|
|||||||
@@ -18,15 +18,7 @@ internal interface IImageCache
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uri">Uri of the item.</param>
|
/// <param name="uri">Uri of the item.</param>
|
||||||
/// <returns>a StorageFile</returns>
|
/// <returns>a StorageFile</returns>
|
||||||
Task<StorageFile?> GetFileFromCacheAsync(Uri uri);
|
Task<StorageFile> GetFileFromCacheAsync(Uri uri);
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// <returns>an instance of Generic type</returns>
|
|
||||||
Task<BitmapImage?> GetFromCacheAsync(Uri uri, bool throwOnError = false);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removed items based on uri list passed
|
/// Removed items based on uri list passed
|
||||||
@@ -34,4 +26,11 @@ internal interface IImageCache
|
|||||||
/// <param name="uriForCachedItems">Enumerable uri list</param>
|
/// <param name="uriForCachedItems">Enumerable uri list</param>
|
||||||
/// <returns>awaitable Task</returns>
|
/// <returns>awaitable Task</returns>
|
||||||
Task RemoveAsync(IEnumerable<Uri> uriForCachedItems);
|
Task RemoveAsync(IEnumerable<Uri> uriForCachedItems);
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
Task RemoveExpiredAsync(TimeSpan? duration = null);
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using CommunityToolkit.WinUI;
|
|
||||||
using Microsoft.UI.Dispatching;
|
|
||||||
using Microsoft.UI.Xaml.Media.Imaging;
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using Windows.Storage;
|
using Windows.Storage;
|
||||||
using Windows.Storage.FileProperties;
|
using Windows.Storage.FileProperties;
|
||||||
@@ -21,10 +18,7 @@ public class ImageCache : CacheBase<BitmapImage>, IImageCache
|
|||||||
{
|
{
|
||||||
private const string DateAccessedProperty = "System.DateAccessed";
|
private const string DateAccessedProperty = "System.DateAccessed";
|
||||||
|
|
||||||
private readonly List<string> extendedPropertyNames = new()
|
private readonly List<string> extendedPropertyNames = new() { DateAccessedProperty };
|
||||||
{
|
|
||||||
DateAccessedProperty,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ImageCache"/> class.
|
/// Initializes a new instance of the <see cref="ImageCache"/> class.
|
||||||
@@ -34,48 +28,6 @@ public class ImageCache : CacheBase<BitmapImage>, IImageCache
|
|||||||
public ImageCache(ILogger<ImageCache> logger, HttpClient httpClient)
|
public ImageCache(ILogger<ImageCache> logger, HttpClient httpClient)
|
||||||
: base(logger, httpClient)
|
: base(logger, httpClient)
|
||||||
{
|
{
|
||||||
DispatcherQueue = Program.UIDispatcherQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets which DispatcherQueue is used to dispatch UI updates.
|
|
||||||
/// </summary>
|
|
||||||
private DispatcherQueue DispatcherQueue { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache specific hooks to process items from HTTP response
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stream">input stream</param>
|
|
||||||
/// <returns>awaitable task</returns>
|
|
||||||
protected override Task<BitmapImage> InitializeTypeAsync(Stream stream)
|
|
||||||
{
|
|
||||||
if (stream.Length == 0)
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return DispatcherQueue.EnqueueAsync(async () =>
|
|
||||||
{
|
|
||||||
BitmapImage image = new();
|
|
||||||
|
|
||||||
// This action will run on the UI thread, no need to care which thread to continue with
|
|
||||||
await image.SetSourceAsync(stream.AsRandomAccessStream()).AsTask().ConfigureAwait(false);
|
|
||||||
|
|
||||||
return image;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache specific hooks to process items from HTTP response
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="baseFile">storage file</param>
|
|
||||||
/// <returns>awaitable task</returns>
|
|
||||||
protected override async Task<BitmapImage> InitializeTypeAsync(StorageFile baseFile)
|
|
||||||
{
|
|
||||||
using (Stream stream = await baseFile.OpenStreamForReadAsync().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
return await InitializeTypeAsync(stream).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ internal static class EventIds
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly EventId MetadataFileMD5Check = 100111;
|
public static readonly EventId MetadataFileMD5Check = 100111;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件缓存
|
||||||
|
/// </summary>
|
||||||
|
public static readonly EventId FileCaching = 100120;
|
||||||
|
|
||||||
// 杂项
|
// 杂项
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -84,4 +84,20 @@ public static class Must
|
|||||||
#pragma warning restore CS8763 // 不应返回标记为 [DoesNotReturn] 的方法。
|
#pragma warning restore CS8763 // 不应返回标记为 [DoesNotReturn] 的方法。
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试抛出任务取消异常
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">取消令牌</param>
|
||||||
|
/// <param name="message">取消消息</param>
|
||||||
|
/// <exception cref="TaskCanceledException">任务被取消</exception>
|
||||||
|
[DebuggerStepThrough]
|
||||||
|
[SuppressMessage("", "CA1068")]
|
||||||
|
public static void TryThrowOnCanceled(CancellationToken token, string message)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw new TaskCanceledException("Image source has changed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,26 @@ internal static class CompositionExtensions
|
|||||||
return brush;
|
return brush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建灰阶效果画刷
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="compositor">合成器</param>
|
||||||
|
/// <param name="source">源</param>
|
||||||
|
/// <returns>合成效果画刷</returns>
|
||||||
|
public static CompositionEffectBrush CompositeGrayScaleEffectBrush(this Compositor compositor, CompositionBrush source)
|
||||||
|
{
|
||||||
|
GrayscaleEffect effect = new()
|
||||||
|
{
|
||||||
|
Source = new CompositionEffectSourceParameter("Source"),
|
||||||
|
};
|
||||||
|
|
||||||
|
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
|
||||||
|
|
||||||
|
brush.SetSourceParameter("Source", source);
|
||||||
|
|
||||||
|
return brush;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建亮度转不透明度效果画刷
|
/// 创建亮度转不透明度效果画刷
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -68,14 +68,6 @@ public sealed partial class MainWindow : Window
|
|||||||
if (!rect.Size.IsEmpty)
|
if (!rect.Size.IsEmpty)
|
||||||
{
|
{
|
||||||
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Create(new POINT(-1, -1), rect, ShowWindowCommand.Normal);
|
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Create(new POINT(-1, -1), rect, ShowWindowCommand.Normal);
|
||||||
//WINDOWPLACEMENT windowPlacement = new()
|
|
||||||
//{
|
|
||||||
// Length = Marshal.SizeOf<WINDOWPLACEMENT>(),
|
|
||||||
// MaxPosition = new POINT(-1, -1),
|
|
||||||
// NormalPosition = rect,
|
|
||||||
// ShowCmd = ShowWindowCommand.Normal,
|
|
||||||
//};
|
|
||||||
|
|
||||||
User32.SetWindowPlacement(handle, ref windowPlacement);
|
User32.SetWindowPlacement(handle, ref windowPlacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,20 +44,7 @@ internal class DescParamDescriptor : IValueConverter
|
|||||||
throw Must.NeverHappen();
|
throw Must.NeverHappen();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IList<string> GetFormattedParameters(IEnumerable<DescFormat> formats, IList<double> param)
|
private static string EvaluateMatch(Match match, IList<double> param)
|
||||||
{
|
|
||||||
List<string> results = new();
|
|
||||||
foreach (DescFormat descFormat in formats)
|
|
||||||
{
|
|
||||||
string format = descFormat.Format;
|
|
||||||
string resultFormatted = Regex.Replace(format, @"{param\d+.*?}", match => EvaluateMatch(match, param));
|
|
||||||
results.Add(resultFormatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string EvaluateMatch(Match match, IList<double> param)
|
|
||||||
{
|
{
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
{
|
{
|
||||||
@@ -87,6 +74,19 @@ internal class DescParamDescriptor : IValueConverter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IList<string> GetFormattedParameters(IEnumerable<DescFormat> formats, IList<double> param)
|
||||||
|
{
|
||||||
|
List<string> results = new();
|
||||||
|
foreach (DescFormat descFormat in formats)
|
||||||
|
{
|
||||||
|
string format = descFormat.Format;
|
||||||
|
string resultFormatted = Regex.Replace(format, @"{param\d+.*?}", match => EvaluateMatch(match, param));
|
||||||
|
results.Add(resultFormatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
private class DescFormat
|
private class DescFormat
|
||||||
{
|
{
|
||||||
public DescFormat(string description, string format)
|
public DescFormat(string description, string format)
|
||||||
|
|||||||
@@ -25,17 +25,17 @@
|
|||||||
<NavigationViewItem
|
<NavigationViewItem
|
||||||
Content="活动"
|
Content="活动"
|
||||||
shvh:NavHelper.NavigateTo="shvp:AnnouncementPage"
|
shvh:NavHelper.NavigateTo="shvp:AnnouncementPage"
|
||||||
Icon="{cwu:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BtnIcon_ActivityEntry.png}"/>
|
Icon="{cwu:BitmapIcon ShowAsMonochrome=True,Source=ms-appx:///Resource/Icon/UI_BtnIcon_ActivityEntry.png}"/>
|
||||||
|
|
||||||
<NavigationViewItem
|
<NavigationViewItem
|
||||||
Content="成就"
|
Content="成就"
|
||||||
shvh:NavHelper.NavigateTo="shvp:AchievementPage"
|
shvh:NavHelper.NavigateTo="shvp:AchievementPage"
|
||||||
Icon="{cwu:BitmapIcon Source=ms-appx:///Resource/Icon/UI_Icon_Achievement.png}"/>
|
Icon="{cwu:BitmapIcon ShowAsMonochrome=True,Source=ms-appx:///Resource/Icon/UI_Icon_Achievement.png}"/>
|
||||||
|
|
||||||
<NavigationViewItem
|
<NavigationViewItem
|
||||||
Content="角色"
|
Content="角色"
|
||||||
shvh:NavHelper.NavigateTo="shvp:WikiAvatarPage"
|
shvh:NavHelper.NavigateTo="shvp:WikiAvatarPage"
|
||||||
Icon="{cwu:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BagTabIcon_Avatar.png}"/>
|
Icon="{cwu:BitmapIcon ShowAsMonochrome=True,Source=ms-appx:///Resource/Icon/UI_BagTabIcon_Avatar.png}"/>
|
||||||
</NavigationView.MenuItems>
|
</NavigationView.MenuItems>
|
||||||
|
|
||||||
<NavigationView.PaneFooter>
|
<NavigationView.PaneFooter>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
|
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
|
||||||
xmlns:cwum="using:CommunityToolkit.WinUI.UI.Media"
|
|
||||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||||
xmlns:shc="using:Snap.Hutao.Control"
|
xmlns:shc="using:Snap.Hutao.Control"
|
||||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||||
@@ -200,12 +199,12 @@
|
|||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<shci:CachedImage
|
<shci:MonoChrome
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Width="27.2"
|
Width="27.2"
|
||||||
Height="27.2"
|
Height="27.2"
|
||||||
Source="{Binding Selected.FetterInfo.VisionBefore,Converter={StaticResource ElementNameIconConverter}}"/>
|
Source="{Binding Selected.FetterInfo.VisionBefore,Converter={StaticResource ElementNameIconConverter}}"/>
|
||||||
<shci:CachedImage
|
<shci:MonoChrome
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="27.2"
|
Width="27.2"
|
||||||
Height="27.2"
|
Height="27.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user