diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index 787ecdfa..0b917cbd 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -44,7 +44,10 @@ public partial class App : Application public static Window? Window { get => window; set => window = value; } /// - public static new App Current => (App)Application.Current; + public static new App Current + { + get => (App)Application.Current; + } /// /// Invoked when the application is launched. diff --git a/src/Snap.Hutao/Snap.Hutao/Context/Database/LogDbContextDesignTimeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Context/Database/LogDbContextDesignTimeFactory.cs index add72b09..532fd516 100644 --- a/src/Snap.Hutao/Snap.Hutao/Context/Database/LogDbContextDesignTimeFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Context/Database/LogDbContextDesignTimeFactory.cs @@ -1,11 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Snap.Hutao.Context.FileSystem; -using Snap.Hutao.Core.Logging; -using Snap.Hutao.Model.Entity; namespace Snap.Hutao.Context.Database; diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnLoadedBehavior.cs b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnLoadedBehavior.cs index 14016952..d5520122 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnLoadedBehavior.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnLoadedBehavior.cs @@ -20,8 +20,8 @@ internal class InvokeCommandOnLoadedBehavior : BehaviorBase /// public ICommand Command { - get { return (ICommand)GetValue(CommandProperty); } - set { SetValue(CommandProperty, value); } + get => (ICommand)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -30,8 +30,8 @@ internal class InvokeCommandOnLoadedBehavior : BehaviorBase [MaybeNull] public object CommandParameter { - get { return GetValue(CommandParameterProperty); } - set { SetValue(CommandParameterProperty, value); } + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs index 954a6284..7d443117 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs @@ -33,7 +33,7 @@ public class CachedImage : ImageEx BitmapImage? image; try { - image = await imageCache.GetFromCacheAsync(imageUri, true, token); + image = await imageCache.GetFromCacheAsync(imageUri, true); } catch (TaskCanceledException) { @@ -54,7 +54,7 @@ public class CachedImage : ImageEx } else { - return Must.NotNull(image); + return Must.NotNull(image!); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/Gradient.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/Gradient.cs index dba607d7..801e836e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/Gradient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/Gradient.cs @@ -1,15 +1,14 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.WinUI.Helpers; using CommunityToolkit.WinUI.UI.Animations; using Microsoft.UI; using Microsoft.UI.Composition; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Hosting; using Microsoft.UI.Xaml.Media; -using Snap.Hutao.Context.FileSystem; using Snap.Hutao.Core; +using Snap.Hutao.Core.Caching; using Snap.Hutao.Core.Threading; using Snap.Hutao.Extension; using Snap.Hutao.Service.Abstraction; @@ -64,6 +63,11 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control .Error(exception, "应用渐变背景时发生异常"); } + private static Task GetCachedFileAsync(string url) + { + return Ioc.Default.GetRequiredService().GetFileFromCacheAsync(new(url)); + } + private void OnSizeChanged(object sender, SizeChangedEventArgs e) { if (e.NewSize != e.PreviousSize && spriteVisual is not null) @@ -76,22 +80,16 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control { if (spriteVisual is not null) { - double width = ActualWidth; - double height = Math.Clamp(width * imageAspectRatio, 0, MaxHeight); - - spriteVisual.Size = new Vector2((float)width, (float)height); - Height = height; + Height = (double)Math.Clamp(ActualWidth / imageAspectRatio, 0, MaxHeight); + spriteVisual.Size = ActualSize; } } private async Task ApplyImageAsync(string url, CancellationToken token) { - await AnimationBuilder - .Create() - .Opacity(0d) - .StartAsync(this, token); + await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token); - StorageFile storageFile = await GetCachedFileAsync(url, token); + StorageFile? storageFile = Must.NotNull((await GetCachedFileAsync(url))!); Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; @@ -104,7 +102,6 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control CompositionEffectBrush gradientEffectBrush = compositor.CompositeBlendEffectBrush(backgroundBrush, foregroundBrush); CompositionEffectBrush opacityMaskEffectBrush = compositor.CompositeLuminanceToAlphaEffectBrush(gradientEffectBrush); - compositor.CreateMaskBrush(); CompositionEffectBrush alphaMaskEffectBrush = compositor.CompositeAlphaMaskEffectBrush(imageSurfaceBrush, opacityMaskEffectBrush); spriteVisual = compositor.CompositeSpriteVisual(alphaMaskEffectBrush); @@ -112,40 +109,7 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control ElementCompositionPreview.SetElementChildVisual(this, spriteVisual); - await AnimationBuilder - .Create() - .Opacity(1d) - .StartAsync(this, token); - } - - private async Task GetCachedFileAsync(string url, CancellationToken token) - { - string fileName = CacheContext.GetCacheFileName(url); - CacheContext cacheContext = Ioc.Default.GetRequiredService(); - StorageFolder imageCacheFolder = await CacheContext - .GetFolderAsync(nameof(Core.Caching.ImageCache), token) - .ConfigureAwait(false); - - StorageFile storageFile; - if (!cacheContext.FileExists(nameof(Core.Caching.ImageCache), fileName)) - { - storageFile = await imageCacheFolder - .CreateFileAsync(fileName) - .AsTask(token) - .ConfigureAwait(false); - await StreamHelper - .GetHttpStreamToStorageFileAsync(new(url), storageFile) - .ConfigureAwait(false); - } - else - { - storageFile = await imageCacheFolder - .GetFileAsync(fileName) - .AsTask(token) - .ConfigureAwait(false); - } - - return storageFile; + await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token); } private async Task LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token) @@ -153,7 +117,7 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token)) { BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token); - imageAspectRatio = (double)decoder.PixelHeight / decoder.PixelWidth; + imageAspectRatio = decoder.PixelWidth / (double)decoder.PixelHeight; return LoadedImageSurface.StartLoadFromStream(imageStream); } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/CacheBase.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/CacheBase.cs index 7f163761..26f277de 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/CacheBase.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/CacheBase.cs @@ -2,537 +2,397 @@ // The .NET Foundation licenses this file to you under the MIT license. // 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; using System.Net.Http; +using System.Text; using Windows.Storage; +using Windows.Storage.FileProperties; -namespace Snap.Hutao.Core.Caching +namespace Snap.Hutao.Core.Caching; + +/// +/// Provides methods and tools to cache files in a folder +/// 经过简化 +/// +/// Generic type as supplied by consumer of the class +public abstract class CacheBase + where T : class { + private readonly SemaphoreSlim cacheFolderSemaphore = new(1); + private readonly ConcurrentDictionary> concurrentTasks = new(); + private readonly ILogger logger; + private readonly HttpClient httpClient; + + private StorageFolder? baseFolder; + private string? cacheFolderName; + private StorageFolder? cacheFolder; + /// - /// Provides methods and tools to cache files in a folder + /// Initializes a new instance of the class. /// - /// Generic type as supplied by consumer of the class - public abstract class CacheBase + /// 日志器 + /// http客户端 + protected CacheBase(ILogger logger, HttpClient httpClient) { - private class ConcurrentRequest - { - public Task Task { get; set; } + this.logger = logger; + this.httpClient = httpClient; - public bool EnsureCachedCopy { get; set; } - } + CacheDuration = TimeSpan.FromDays(30); + RetryCount = 3; + } - private readonly SemaphoreSlim _cacheFolderSemaphore = new(1); - private StorageFolder _baseFolder = null; - private string _cacheFolderName = null; + /// + /// Gets or sets the life duration of every cache entry. + /// + public TimeSpan CacheDuration { get; set; } - private StorageFolder _cacheFolder = null; - private InMemoryStorage _inMemoryFileStorage = null; + /// + /// Gets or sets the number of retries trying to ensure the file is cached. + /// + public uint RetryCount { get; set; } - private ConcurrentDictionary _concurrentTasks = new ConcurrentDictionary(); + /// + /// Clears all files in the cache + /// + /// awaitable task + public async Task ClearAsync() + { + StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false); + IReadOnlyList files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false); - private HttpClient _httpClient = null; + await InternalClearAsync(files).ConfigureAwait(false); + } - /// - /// Initializes a new instance of the class. - /// - protected CacheBase() - { - CacheDuration = TimeSpan.FromDays(1); - _inMemoryFileStorage = new InMemoryStorage(); - RetryCount = 1; - } + /// + /// Removes cached files that have expired + /// + /// Optional timespan to compute whether file has expired or not. If no value is supplied, is used. + /// awaitable task + public async Task RemoveExpiredAsync(TimeSpan? duration = null) + { + TimeSpan expiryDuration = duration ?? CacheDuration; - /// - /// Gets or sets the life duration of every cache entry. - /// - public TimeSpan CacheDuration { get; set; } + StorageFolder? folder = await GetCacheFolderAsync().ConfigureAwait(false); + IReadOnlyList? files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false); - /// - /// Gets or sets the number of retries trying to ensure the file is cached. - /// - public uint RetryCount { get; set; } + List? filesToDelete = new(); - /// - /// Gets or sets max in-memory item storage count - /// - public int MaxMemoryCacheCount - { - get - { - return _inMemoryFileStorage.MaxItemCount; - } - - set - { - _inMemoryFileStorage.MaxItemCount = value; - } - } - - /// - /// Gets instance of - /// - protected HttpClient HttpClient - { - get - { - if (_httpClient == null) - { - var messageHandler = new HttpClientHandler() { MaxConnectionsPerServer = 20 }; - - _httpClient = new HttpClient(messageHandler); - } - - return _httpClient; - } - } - - /// - /// Initializes FileCache and provides root folder and cache folder name - /// - /// Folder that is used as root for cache - /// Cache folder name - /// instance of - /// awaitable task - public virtual async Task InitializeAsync(StorageFolder folder = null, string folderName = null, HttpMessageHandler httpMessageHandler = null) - { - _baseFolder = folder; - _cacheFolderName = folderName; - - _cacheFolder = await GetCacheFolderAsync().ConfigureAwait(false); - - if (httpMessageHandler != null) - { - _httpClient = new HttpClient(httpMessageHandler); - } - } - - /// - /// Clears all files in the cache - /// - /// awaitable task - public async Task ClearAsync() - { - var folder = await GetCacheFolderAsync().ConfigureAwait(false); - var files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false); - - await InternalClearAsync(files).ConfigureAwait(false); - - _inMemoryFileStorage.Clear(); - } - - /// - /// Clears file if it has expired - /// - /// timespan to compute whether file has expired or not - /// awaitable task - public Task ClearAsync(TimeSpan duration) - { - return RemoveExpiredAsync(duration); - } - - /// - /// Removes cached files that have expired - /// - /// Optional timespan to compute whether file has expired or not. If no value is supplied, is used. - /// awaitable task - public async Task RemoveExpiredAsync(TimeSpan? duration = null) - { - TimeSpan expiryDuration = duration ?? CacheDuration; - - var folder = await GetCacheFolderAsync().ConfigureAwait(false); - var files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false); - - var filesToDelete = new List(); - - 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); - } - - /// - /// Removed items based on uri list passed - /// - /// Enumerable uri list - /// awaitable Task - public async Task RemoveAsync(IEnumerable uriForCachedItems) - { - if (uriForCachedItems == null || !uriForCachedItems.Any()) - { - return; - } - - var folder = await GetCacheFolderAsync().ConfigureAwait(false); - var files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false); - - var filesToDelete = new List(); - var keys = new List(); - - Dictionary hashDictionary = new Dictionary(); - - foreach (var file in files) - { - hashDictionary.Add(file.Name, 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); - } - - /// - /// Assures that item represented by Uri is cached. - /// - /// Uri of the item - /// Indicates whether or not exception should be thrown if item cannot be cached - /// Indicates if item should be loaded into the in-memory storage - /// instance of - /// Awaitable Task - public Task PreCacheAsync(Uri uri, bool throwOnError = false, bool storeToMemoryCache = false, CancellationToken cancellationToken = default(CancellationToken)) - { - return GetItemAsync(uri, throwOnError, !storeToMemoryCache, cancellationToken, null); - } - - /// - /// 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. - /// - /// Uri of the item. - /// Indicates whether or not exception should be thrown if item cannot be found / downloaded. - /// instance of - /// key value pairs used when initializing instance of generic type - /// an instance of Generic type - public Task GetFromCacheAsync(Uri uri, bool throwOnError = false, CancellationToken cancellationToken = default(CancellationToken), List> initializerKeyValues = null) - { - return GetItemAsync(uri, throwOnError, false, cancellationToken, initializerKeyValues); - } - - /// - /// Gets the StorageFile containing cached item for given Uri - /// - /// Uri of the item. - /// a StorageFile - public async Task GetFileFromCacheAsync(Uri uri) - { - var folder = await GetCacheFolderAsync().ConfigureAwait(false); - - string fileName = GetCacheFileName(uri); - - var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false); - - return item as StorageFile; - } - - /// - /// 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. - /// - /// Uri of the item. - /// an instance of Generic type - 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; - } - - /// - /// Cache specific hooks to process items from HTTP response - /// - /// input stream - /// key value pairs used when initializing instance of generic type - /// awaitable task - protected abstract Task InitializeTypeAsync(Stream stream, List> initializerKeyValues = null); - - /// - /// Cache specific hooks to process items from HTTP response - /// - /// storage file - /// key value pairs used when initializing instance of generic type - /// awaitable task - protected abstract Task InitializeTypeAsync(StorageFile baseFile, List> initializerKeyValues = null); - - /// - /// Override-able method that checks whether file is valid or not. - /// - /// storage file - /// cache duration - /// option to mark uninitialized file as expired - /// bool indicate whether file has expired or not - protected virtual async Task IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true) + foreach (StorageFile? file in files) { if (file == null) { - return treatNullFileAsOutOfDate; + continue; } - var properties = await file.GetBasicPropertiesAsync().AsTask().ConfigureAwait(false); - - return properties.Size == 0 || DateTime.Now.Subtract(properties.DateModified.DateTime) > duration; - } - - private static string GetCacheFileName(Uri uri) - { - return CreateHash64(uri.ToString()).ToString(); - } - - private static ulong CreateHash64(string str) - { - byte[] utf8 = global::System.Text.Encoding.UTF8.GetBytes(str); - - ulong value = (ulong)utf8.Length; - for (int n = 0; n < utf8.Length; n++) + if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false)) { - value += (ulong)utf8[n] << ((n * 5) % 56); + filesToDelete.Add(file); } - - return value; } - private async Task GetItemAsync(Uri uri, bool throwOnError, bool preCacheOnly, CancellationToken cancellationToken, List> initializerKeyValues) - { - T instance = default(T); + await InternalClearAsync(filesToDelete).ConfigureAwait(false); + } + /// + /// Removed items based on uri list passed + /// + /// Enumerable uri list + /// awaitable Task + public async Task RemoveAsync(IEnumerable uriForCachedItems) + { + if (uriForCachedItems == null || !uriForCachedItems.Any()) + { + return; + } + + StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false); + IReadOnlyList files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false); + + List filesToDelete = new(); + List keys = new(); + + Dictionary hashDictionary = new(); + + foreach (StorageFile file in files) + { + hashDictionary.Add(file.Name, file); + } + + foreach (Uri uri in uriForCachedItems) + { 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 (hashDictionary.TryGetValue(fileName, out StorageFile? file)) { - await request.Task.ConfigureAwait(false); - request = null; + filesToDelete.Add(file); + keys.Add(fileName); } + } - if (request == null) + await InternalClearAsync(filesToDelete).ConfigureAwait(false); + } + + /// + /// 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. + /// + /// Uri of the item. + /// Indicates whether or not exception should be thrown if item cannot be found / downloaded. + /// an instance of Generic type + public Task GetFromCacheAsync(Uri uri, bool throwOnError = false) + { + return GetItemAsync(uri, throwOnError); + } + + /// + /// Gets the StorageFile containing cached item for given Uri + /// + /// Uri of the item. + /// a StorageFile + public async Task GetFileFromCacheAsync(Uri uri) + { + StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false); + + string fileName = GetCacheFileName(uri); + + IStorageItem? item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false); + + if (item == null) + { + 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; + } + + /// + /// Cache specific hooks to process items from HTTP response + /// + /// input stream + /// awaitable task + protected abstract Task InitializeTypeAsync(Stream stream); + + /// + /// Cache specific hooks to process items from HTTP response + /// + /// storage file + /// awaitable task + protected abstract Task InitializeTypeAsync(StorageFile baseFile); + + /// + /// Override-able method that checks whether file is valid or not. + /// + /// storage file + /// cache duration + /// option to mark uninitialized file as expired + /// bool indicate whether file has expired or not + protected virtual async Task IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true) + { + if (file == null) + { + return treatNullFileAsOutOfDate; + } + + BasicProperties? properties = await file.GetBasicPropertiesAsync().AsTask().ConfigureAwait(false); + + return properties.Size == 0 || DateTime.Now.Subtract(properties.DateModified.DateTime) > duration; + } + + private static string GetCacheFileName(Uri uri) + { + return CreateHash64(uri.ToString()).ToString(); + } + + private static ulong CreateHash64(string str) + { + byte[] utf8 = 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 GetItemAsync(Uri uri, bool throwOnError) + { + T? instance = default(T); + + string fileName = GetCacheFileName(uri); + concurrentTasks.TryGetValue(fileName, out Task? request); + + // complete previous task first + if (request != null) + { + 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) { - request = new ConcurrentRequest() - { - Task = GetFromCacheOrDownloadAsync(uri, fileName, preCacheOnly, cancellationToken, initializerKeyValues), - EnsureCachedCopy = preCacheOnly - }; - - _concurrentTasks[fileName] = request; + throw; } + } + finally + { + concurrentTasks.TryRemove(fileName, out _); + } + return instance; + } + + private async Task 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 { - instance = await request.Task.ConfigureAwait(false); - } - catch (Exception ex) - { - global::System.Diagnostics.Debug.WriteLine(ex.Message); - if (throwOnError) + while (retries < RetryCount) { - throw; - } - } - finally - { - _concurrentTasks.TryRemove(fileName, out _); - } - - return instance; - } - - private async Task GetFromCacheOrDownloadAsync(Uri uri, string fileName, bool preCacheOnly, CancellationToken cancellationToken, List> initializerKeyValues) - { - 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 = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false) as StorageFile; - - bool downloadDataFile = baseFile == null || await IsFileOutOfDateAsync(baseFile, CacheDuration).ConfigureAwait(false); - - if (baseFile == null) - { - baseFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false); - } - - if (downloadDataFile) - { - uint retries = 0; - try - { - while (retries < RetryCount) + try { - try - { - instance = await DownloadFileAsync(uri, baseFile, preCacheOnly, cancellationToken, initializerKeyValues).ConfigureAwait(false); + instance = await DownloadFileAsync(uri, baseFile).ConfigureAwait(false); - if (instance != null) - { - break; - } - } - catch (FileNotFoundException) + if (instance != null) { + break; } - - retries++; } - } - catch (Exception) - { - await baseFile.DeleteAsync().AsTask().ConfigureAwait(false); - throw; // re-throwing the exception changes the stack trace. just throw - } - } - - if (EqualityComparer.Default.Equals(instance, default(T)) && !preCacheOnly) - { - instance = await InitializeTypeAsync(baseFile, initializerKeyValues).ConfigureAwait(false); - - if (_inMemoryFileStorage.MaxItemCount > 0) - { - var properties = await baseFile.GetBasicPropertiesAsync().AsTask().ConfigureAwait(false); - - var msi = new InMemoryStorageItem(fileName, properties.DateModified.DateTime, instance); - _inMemoryFileStorage.SetItem(msi); - } - } - - return instance; - } - - private async Task DownloadFileAsync(Uri uri, StorageFile baseFile, bool preCacheOnly, CancellationToken cancellationToken, List> initializerKeyValues) - { - 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 = await baseFile.OpenStreamForWriteAsync()) + catch (FileNotFoundException) { - 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 InitializeTypeAsync(ms, initializerKeyValues).ConfigureAwait(false); + retries++; } } - - return instance; - } - - private async Task InternalClearAsync(IEnumerable files) - { - foreach (var file in files) + catch (Exception) { - try - { - await file.DeleteAsync().AsTask().ConfigureAwait(false); - } - catch - { - // Just ignore errors for now} - } + await baseFile.DeleteAsync().AsTask().ConfigureAwait(false); + throw; // re-throwing the exception changes the stack trace. just throw } } - /// - /// Initializes with default values if user has not initialized explicitly - /// - /// awaitable task - private async Task ForceInitialiseAsync() + if (EqualityComparer.Default.Equals(instance, default(T))) { - if (_cacheFolder != null) + instance = await InitializeTypeAsync(baseFile).ConfigureAwait(false); + } + + return instance; + } + + private async Task DownloadFileAsync(Uri uri, StorageFile baseFile) + { + T? instance = default; + + using (MemoryStream memory = new()) + { + using (Stream httpStream = await httpClient.GetStreamAsync(uri)) { - return; + 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; + } } - await _cacheFolderSemaphore.WaitAsync().ConfigureAwait(false); + instance = await InitializeTypeAsync(memory).ConfigureAwait(false); + } - _inMemoryFileStorage = new InMemoryStorage(); - - if (_baseFolder == null) - { - _baseFolder = ApplicationData.Current.TemporaryFolder; - } - - if (string.IsNullOrWhiteSpace(_cacheFolderName)) - { - _cacheFolderName = GetType().Name; - } + return instance; + } + [SuppressMessage("", "CA1822")] + private async Task InternalClearAsync(IEnumerable files) + { + foreach (StorageFile file in files) + { try { - _cacheFolder = await _baseFolder.CreateFolderAsync(_cacheFolderName, CreationCollisionOption.OpenIfExists).AsTask().ConfigureAwait(false); + await file.DeleteAsync().AsTask().ConfigureAwait(false); } - finally + catch { - _cacheFolderSemaphore.Release(); + // Just ignore errors for now } } - - private async Task GetCacheFolderAsync() - { - if (_cacheFolder == null) - { - await ForceInitialiseAsync().ConfigureAwait(false); - } - - return _cacheFolder; - } + } + + /// + /// Initializes with default values if user has not initialized explicitly + /// + /// awaitable task + private async Task InitializeInternalAsync() + { + if (cacheFolder != null) + { + return; + } + + await cacheFolderSemaphore.WaitAsync().ConfigureAwait(false); + + baseFolder ??= ApplicationData.Current.TemporaryFolder; + + if (string.IsNullOrWhiteSpace(cacheFolderName)) + { + cacheFolderName = GetType().Name; + } + + try + { + cacheFolder = await baseFolder + .CreateFolderAsync(cacheFolderName, CreationCollisionOption.OpenIfExists) + .AsTask() + .ConfigureAwait(false); + } + finally + { + cacheFolderSemaphore.Release(); + } + } + + private async Task GetCacheFolderAsync() + { + if (cacheFolder == null) + { + await InitializeInternalAsync().ConfigureAwait(false); + } + + return Must.NotNull(cacheFolder!); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs index 6bd710eb..a0383a45 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/IImageCache.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Xaml.Media.Imaging; using System.Collections.Generic; +using Windows.Storage; namespace Snap.Hutao.Core.Caching; @@ -12,16 +13,20 @@ namespace Snap.Hutao.Core.Caching; /// 缓存类型 internal interface IImageCache { + /// + /// Gets the StorageFile containing cached item for given Uri + /// + /// Uri of the item. + /// a StorageFile + Task GetFileFromCacheAsync(Uri uri); + /// /// 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. /// /// Uri of the item. /// Indicates whether or not exception should be thrown if item cannot be found / downloaded. - /// instance of - /// key value pairs used when initializing instance of generic type /// an instance of Generic type - [SuppressMessage("", "CA1068")] - Task GetFromCacheAsync(Uri uri, bool throwOnError = false, CancellationToken cancellationToken = default(CancellationToken), List> initializerKeyValues = null!); + Task GetFromCacheAsync(Uri uri, bool throwOnError = false); /// /// Removed items based on uri list passed diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs index df5971d1..4a27b7d2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs @@ -2,12 +2,11 @@ // Licensed under the MIT license. using CommunityToolkit.WinUI; -using CommunityToolkit.WinUI.UI; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media.Imaging; using System.Collections.Generic; using System.IO; -using System.Reflection; +using System.Net.Http; using Windows.Storage; using Windows.Storage.FileProperties; @@ -30,13 +29,12 @@ public class ImageCache : CacheBase, IImageCache /// /// Initializes a new instance of the class. /// + /// 日志器 /// http客户端 - public ImageCache() + public ImageCache(ILogger logger, HttpClient httpClient) + : base(logger, httpClient) { DispatcherQueue = Program.UIDispatcherQueue; - - CacheDuration = TimeSpan.FromDays(30); - RetryCount = 3; } /// @@ -48,9 +46,8 @@ public class ImageCache : CacheBase, IImageCache /// Cache specific hooks to process items from HTTP response /// /// input stream - /// key value pairs used when initializing instance of generic type /// awaitable task - protected override Task InitializeTypeAsync(Stream stream, List> initializerKeyValues = null!) + protected override Task InitializeTypeAsync(Stream stream) { if (stream.Length == 0) { @@ -61,24 +58,6 @@ public class ImageCache : CacheBase, IImageCache { BitmapImage image = new(); - if (initializerKeyValues != null && initializerKeyValues.Count > 0) - { - foreach (KeyValuePair kvp in initializerKeyValues) - { - if (string.IsNullOrWhiteSpace(kvp.Key)) - { - continue; - } - - PropertyInfo? propInfo = image.GetType().GetProperty(kvp.Key, BindingFlags.Public | BindingFlags.Instance); - - if (propInfo != null && propInfo.CanWrite) - { - propInfo.SetValue(image, kvp.Value); - } - } - } - // 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); @@ -90,13 +69,12 @@ public class ImageCache : CacheBase, IImageCache /// Cache specific hooks to process items from HTTP response /// /// storage file - /// key value pairs used when initializing instance of generic type /// awaitable task - protected override async Task InitializeTypeAsync(StorageFile baseFile, List> initializerKeyValues = null!) + protected override async Task InitializeTypeAsync(StorageFile baseFile) { using (Stream stream = await baseFile.OpenStreamForReadAsync().ConfigureAwait(false)) { - return await InitializeTypeAsync(stream, initializerKeyValues).ConfigureAwait(false); + return await InitializeTypeAsync(stream).ConfigureAwait(false); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs index 432da6ab..0a6ec2c7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/ValueStopwatch.cs @@ -22,13 +22,19 @@ internal struct ValueStopwatch /// /// 是否处于活动状态 /// - public bool IsActive => startTimestamp != 0; + public bool IsActive + { + get => startTimestamp != 0; + } /// /// 触发一个新的停表 /// /// 一个新的停表实例 - public static ValueStopwatch StartNew() => new(Stopwatch.GetTimestamp()); + public static ValueStopwatch StartNew() + { + return new(Stopwatch.GetTimestamp()); + } /// /// 获取经过的时间 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs index bb7dffb7..274bc70f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs @@ -30,6 +30,11 @@ internal static class EventIds /// public static readonly EventId WebView2EnvironmentException = 100003; + /// + /// 缓存异常 + /// + public static readonly EventId CacheException = 100004; + // 服务 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntry.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntry.cs index 48465702..cbc4b4d6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntry.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/LogEntry.cs @@ -1,7 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Context.Database; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs b/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs index c15e2989..ea134c12 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/WebView2Helper.cs @@ -50,5 +50,8 @@ internal abstract class WebView2Helper /// /// WebView2的版本 /// - public static string Version => version; + public static string Version + { + get => version; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Win32/WINDOWPLACEMENT.cs b/src/Snap.Hutao/Snap.Hutao/Core/Win32/WINDOWPLACEMENT.cs index 033a0950..0d10be88 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Win32/WINDOWPLACEMENT.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Win32/WINDOWPLACEMENT.cs @@ -57,4 +57,22 @@ internal struct WINDOWPLACEMENT return result; } } + + /// + /// 构造一个新的 + /// + /// 最大点 + /// 正常位置 + /// 显示命令 + /// 窗体位置 + public static WINDOWPLACEMENT Create(POINT max, RECT normal, ShowWindowCommand command) + { + WINDOWPLACEMENT result = Default; + + result.MaxPosition = max; + result.NormalPosition = normal; + result.ShowCmd = command; + + return result; + } } diff --git a/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs index 186047aa..e26699ff 100644 --- a/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Microsoft.Extensions.DependencyInjection; -using Snap.Hutao.Core.Caching; using Snap.Hutao.Service.Metadata; using Snap.Hutao.Web.Enka; using Snap.Hutao.Web.Hoyolab.Bbs.User; diff --git a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs index 15c5e74b..b7d5fa0c 100644 --- a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs @@ -67,13 +67,14 @@ public sealed partial class MainWindow : Window RECT rect = RetriveWindowRect(); if (!rect.Size.IsEmpty) { - WINDOWPLACEMENT windowPlacement = new() - { - Length = Marshal.SizeOf(), - MaxPosition = new POINT(-1, -1), - NormalPosition = rect, - ShowCmd = ShowWindowCommand.Normal, - }; + WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Create(new POINT(-1, -1), rect, ShowWindowCommand.Normal); + //WINDOWPLACEMENT windowPlacement = new() + //{ + // Length = Marshal.SizeOf(), + // MaxPosition = new POINT(-1, -1), + // NormalPosition = rect, + // ShowCmd = ShowWindowCommand.Normal, + //}; User32.SetWindowPlacement(handle, ref windowPlacement); } diff --git a/src/Snap.Hutao/Snap.Hutao/Program.cs b/src/Snap.Hutao/Snap.Hutao/Program.cs index 0ec7dffd..f13b5d71 100644 --- a/src/Snap.Hutao/Snap.Hutao/Program.cs +++ b/src/Snap.Hutao/Snap.Hutao/Program.cs @@ -18,7 +18,10 @@ public static class Program /// /// 主线程调度器队列 /// - public static DispatcherQueue UIDispatcherQueue => Must.NotNull(dispatcherQueue!); + public static DispatcherQueue UIDispatcherQueue + { + get => Must.NotNull(dispatcherQueue!); + } [DllImport("Microsoft.ui.xaml.dll")] private static extern void XamlCheckProcessRequirements(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs index f3a76568..5aeee539 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Caching.Memory; using Snap.Hutao.Context.FileSystem; using Snap.Hutao.Core.Abstraction; using Snap.Hutao.Core.Logging; -using Snap.Hutao.Extension; using Snap.Hutao.Model.Metadata.Achievement; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Reliquary; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Helper/NavHelper.cs b/src/Snap.Hutao/Snap.Hutao/View/Helper/NavHelper.cs index 59b8516d..18a20073 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Helper/NavHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Helper/NavHelper.cs @@ -4,7 +4,6 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.Core; -using Snap.Hutao.Service.Navigation; namespace Snap.Hutao.View.Helper; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs index 335e41ae..6d13a137 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs @@ -36,10 +36,7 @@ public struct PlayerUid /// public string Region { - get - { - return region ??= EvaluateRegion(Value[0]); - } + get => region ??= EvaluateRegion(Value[0]); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/Damage.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/Damage.cs index b0e5aa4b..2e9e9415 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/Damage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/Damage.cs @@ -1,8 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using System.Collections.Generic; - namespace Snap.Hutao.Web.Hutao.Model.Post; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/PlayerRecord.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/PlayerRecord.cs index 84fe59f2..20ad843c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/PlayerRecord.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/Post/PlayerRecord.cs @@ -84,11 +84,11 @@ public class PlayerRecord return hutaoClient.UploadRecordAsync(this, token); } - private static Damage? GetDamage(List ranks) + private static Damage? GetDamage(List ranks) { if (ranks.Count > 0) { - RankInfo rank = ranks[0]; + Rank rank = ranks[0]; return new Damage(rank.AvatarId, rank.Value); } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/RankInfo.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/RankInfo.cs index 09756eeb..9aba85d5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/RankInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/RankInfo.cs @@ -1,10 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Web.Hutao.Model.Converter; -using System.Collections.Generic; -using System.Text.Json.Serialization; - namespace Snap.Hutao.Web.Hutao.Model; /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/RankInfoWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/RankInfoWrapper.cs index ec8d46f6..a559c6d2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/RankInfoWrapper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/RankInfoWrapper.cs @@ -1,10 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Web.Hutao.Model.Converter; -using System.Collections.Generic; -using System.Text.Json.Serialization; - namespace Snap.Hutao.Web.Hutao.Model; ///