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 Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Extension;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
|
||||
@@ -14,15 +15,11 @@ namespace Snap.Hutao.Control.Image;
|
||||
/// </summary>
|
||||
public class CachedImage : ImageEx
|
||||
{
|
||||
private readonly IImageCache imageCache;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的缓存图像
|
||||
/// </summary>
|
||||
public CachedImage()
|
||||
{
|
||||
imageCache = Ioc.Default.GetRequiredService<IImageCache>();
|
||||
|
||||
IsCacheEnabled = true;
|
||||
EnableLazyLoading = true;
|
||||
}
|
||||
@@ -30,10 +27,15 @@ public class CachedImage : ImageEx
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<ImageSource> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
BitmapImage? image;
|
||||
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -43,18 +45,8 @@ public class CachedImage : ImageEx
|
||||
catch
|
||||
{
|
||||
// maybe the image is corrupted, remove it.
|
||||
await imageCache.RemoveAsync(imageUri.Enumerate());
|
||||
await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false);
|
||||
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
|
||||
.GetRequiredService<IInfoBarService>()
|
||||
.Error(exception, "应用渐变背景时发生异常");
|
||||
.Error(exception, "应用渐变图像时发生异常");
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -23,7 +22,6 @@ public abstract class CacheBase<T>
|
||||
where T : class
|
||||
{
|
||||
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
|
||||
private readonly ConcurrentDictionary<string, Task<T?>> concurrentTasks = new();
|
||||
private readonly ILogger logger;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -48,12 +46,12 @@ public abstract class CacheBase<T>
|
||||
/// <summary>
|
||||
/// Gets or sets the life duration of every cache entry.
|
||||
/// </summary>
|
||||
public TimeSpan CacheDuration { get; set; }
|
||||
public TimeSpan CacheDuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of retries trying to ensure the file is cached.
|
||||
/// </summary>
|
||||
public uint RetryCount { get; set; }
|
||||
public uint RetryCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Clears all files in the cache
|
||||
@@ -76,12 +74,12 @@ public abstract class CacheBase<T>
|
||||
{
|
||||
TimeSpan expiryDuration = duration ?? CacheDuration;
|
||||
|
||||
StorageFolder? folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
||||
IReadOnlyList<StorageFile>? files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
|
||||
StorageFolder folder = await GetCacheFolderAsync().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)
|
||||
{
|
||||
@@ -135,24 +133,14 @@ public abstract class CacheBase<T>
|
||||
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>
|
||||
/// Gets the StorageFile containing cached item for given Uri
|
||||
/// </summary>
|
||||
/// <param name="uri">Uri of the item.</param>
|
||||
/// <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);
|
||||
|
||||
string fileName = GetCacheFileName(uri);
|
||||
@@ -161,28 +149,14 @@ public abstract class CacheBase<T>
|
||||
|
||||
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);
|
||||
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>
|
||||
/// Override-able method that checks whether file is valid or not.
|
||||
/// </summary>
|
||||
@@ -220,121 +194,16 @@ public abstract class CacheBase<T>
|
||||
return value;
|
||||
}
|
||||
|
||||
private async Task<T?> GetItemAsync(Uri uri, bool throwOnError)
|
||||
private async Task DownloadFileAsync(Uri uri, StorageFile baseFile)
|
||||
{
|
||||
T? instance = default(T);
|
||||
|
||||
string fileName = GetCacheFileName(uri);
|
||||
concurrentTasks.TryGetValue(fileName, out Task<T?>? request);
|
||||
|
||||
// complete previous task first
|
||||
if (request != null)
|
||||
using (Stream httpStream = await httpClient.GetStreamAsync(uri))
|
||||
{
|
||||
await request.ConfigureAwait(false);
|
||||
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)
|
||||
using (Stream fs = await baseFile.OpenStreamForWriteAsync())
|
||||
{
|
||||
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")]
|
||||
|
||||
@@ -18,15 +18,7 @@ internal interface IImageCache
|
||||
/// </summary>
|
||||
/// <param name="uri">Uri of the item.</param>
|
||||
/// <returns>a StorageFile</returns>
|
||||
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);
|
||||
Task<StorageFile> GetFileFromCacheAsync(Uri uri);
|
||||
|
||||
/// <summary>
|
||||
/// Removed items based on uri list passed
|
||||
@@ -34,4 +26,11 @@ internal interface IImageCache
|
||||
/// <param name="uriForCachedItems">Enumerable uri list</param>
|
||||
/// <returns>awaitable Task</returns>
|
||||
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.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.FileProperties;
|
||||
@@ -21,10 +18,7 @@ public class ImageCache : CacheBase<BitmapImage>, IImageCache
|
||||
{
|
||||
private const string DateAccessedProperty = "System.DateAccessed";
|
||||
|
||||
private readonly List<string> extendedPropertyNames = new()
|
||||
{
|
||||
DateAccessedProperty,
|
||||
};
|
||||
private readonly List<string> extendedPropertyNames = new() { DateAccessedProperty };
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
: 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>
|
||||
|
||||
@@ -57,6 +57,11 @@ internal static class EventIds
|
||||
/// </summary>
|
||||
public static readonly EventId MetadataFileMD5Check = 100111;
|
||||
|
||||
/// <summary>
|
||||
/// 文件缓存
|
||||
/// </summary>
|
||||
public static readonly EventId FileCaching = 100120;
|
||||
|
||||
// 杂项
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -84,4 +84,20 @@ public static class Must
|
||||
#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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
||||
@@ -68,14 +68,6 @@ public sealed partial class MainWindow : Window
|
||||
if (!rect.Size.IsEmpty)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,20 +44,7 @@ internal class DescParamDescriptor : IValueConverter
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
|
||||
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 string EvaluateMatch(Match match, IList<double> param)
|
||||
private static string EvaluateMatch(Match match, IList<double> param)
|
||||
{
|
||||
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
|
||||
{
|
||||
public DescFormat(string description, string format)
|
||||
|
||||
@@ -25,17 +25,17 @@
|
||||
<NavigationViewItem
|
||||
Content="活动"
|
||||
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
|
||||
Content="成就"
|
||||
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
|
||||
Content="角色"
|
||||
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.PaneFooter>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:cwum="using:CommunityToolkit.WinUI.UI.Media"
|
||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:shc="using:Snap.Hutao.Control"
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
@@ -200,12 +199,12 @@
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shci:CachedImage
|
||||
<shci:MonoChrome
|
||||
Grid.Column="0"
|
||||
Width="27.2"
|
||||
Height="27.2"
|
||||
Source="{Binding Selected.FetterInfo.VisionBefore,Converter={StaticResource ElementNameIconConverter}}"/>
|
||||
<shci:CachedImage
|
||||
<shci:MonoChrome
|
||||
Grid.Column="1"
|
||||
Width="27.2"
|
||||
Height="27.2"
|
||||
|
||||
Reference in New Issue
Block a user