update to SDK 1.1.3

This commit is contained in:
DismissedLight
2022-07-23 12:54:26 +08:00
parent f36f555f70
commit 2a5c92e242
66 changed files with 1905 additions and 340 deletions

View File

@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.2" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.3" />
</ItemGroup>
<ItemGroup>

View File

@@ -28,4 +28,8 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="CoreEnvironment\" />
</ItemGroup>
</Project>

View File

@@ -13,8 +13,6 @@
<!--Modify Window title bar color-->
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush" />
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush" />
<ThemeResource x:Key="WindowCaptionForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<ThemeResource x:Key="WindowCaptionForegroundDisabled" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush" />

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.UI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.VisualStudio.Threading;
@@ -32,8 +31,9 @@ public partial class App : Application
// load app resource
InitializeComponent();
InitializeDependencyInjection();
InitializeImageCache();
// notice that we already call InitializeDependencyInjection() above
// so we can use Ioc here.
logger = Ioc.Default.GetRequiredService<ILogger<App>>();
UnhandledException += AppUnhandledException;
}
@@ -43,6 +43,9 @@ public partial class App : Application
/// </summary>
public static Window? Window { get => window; set => window = value; }
/// <inheritdoc cref="Application"/>
public static new App Current => (App)Application.Current;
/// <summary>
/// Invoked when the application is launched.
/// </summary>
@@ -71,12 +74,13 @@ public partial class App : Application
Window = Ioc.Default.GetRequiredService<MainWindow>();
Window.Activate();
logger.LogInformation("Cache folder : {folder}", Windows.Storage.ApplicationData.Current.TemporaryFolder.Path);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", Windows.Storage.ApplicationData.Current.TemporaryFolder.Path);
if (Ioc.Default.GetRequiredService<IMetadataService>() is IMetadataInitializer initializer)
{
initializer.InitializeInternalAsync().SafeForget(logger: logger);
}
Ioc.Default
.GetRequiredService<IMetadataService>()
.As<IMetadataInitializer>()?
.InitializeInternalAsync()
.SafeForget(logger: logger);
if (uri != null)
{
@@ -90,7 +94,9 @@ public partial class App : Application
IServiceProvider services = new ServiceCollection()
// Microsoft extension
.AddLogging(builder => builder.AddDebug())
.AddLogging(builder => builder
.AddDebug()
.AddDatabase())
.AddMemoryCache()
// Hutao extensions
@@ -107,12 +113,6 @@ public partial class App : Application
Ioc.Default.ConfigureServices(services);
}
private static void InitializeImageCache()
{
ImageCache.Instance.CacheDuration = TimeSpan.FromDays(30);
ImageCache.Instance.RetryCount = 3;
}
private void AppUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Logging;
namespace Snap.Hutao.Context.Database;
/// <summary>
/// 日志数据库上下文
/// </summary>
public class LogDbContext : DbContext
{
/// <summary>
/// 创建一个新的
/// </summary>
/// <param name="options">选项</param>
private LogDbContext(DbContextOptions<LogDbContext> options)
: base(options)
{
}
/// <summary>
/// 日志记录
/// </summary>
public DbSet<LogEntry> Logs { get; set; } = default!;
/// <summary>
/// 构造一个临时的日志数据库上下文
/// </summary>
/// <param name="sqlConnectionString">连接字符串</param>
/// <returns>日志数据库上下文</returns>
public static LogDbContext Create(string sqlConnectionString)
{
return new(new DbContextOptionsBuilder<LogDbContext>().UseSqlite(sqlConnectionString).Options);
}
}

View File

@@ -0,0 +1,25 @@
// 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;
/// <summary>
/// 此类只用于在生成迁移时提供数据库上下文
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public class LogDbContextDesignTimeFactory : IDesignTimeDbContextFactory<LogDbContext>
{
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public LogDbContext CreateDbContext(string[] args)
{
MyDocumentContext myDocument = new(new());
return LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}");
}
}

View File

@@ -24,9 +24,13 @@ internal class CacheContext : FileSystemContext
/// <summary>
/// 获取缓存文件夹
/// </summary>
public static StorageFolder Folder
/// <param name="folderName">文件夹名称</param>
/// <param name="token">取消令牌</param>
/// <returns>缓存文件夹</returns>
public static Task<StorageFolder> GetFolderAsync(string folderName, CancellationToken token)
{
get => ApplicationData.Current.TemporaryFolder;
StorageFolder tempstate = ApplicationData.Current.TemporaryFolder;
return tempstate.CreateFolderAsync(folderName, CreationCollisionOption.OpenIfExists).AsTask(token);
}
/// <summary>

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>

View File

@@ -17,7 +17,7 @@ public class SystemBackdrop
{
private readonly Window window;
private WindowsSystemDispatcherQueueHelper? dispatcherQueueHelper;
private DispatcherQueueHelper? dispatcherQueueHelper;
private MicaController? backdropController;
private SystemBackdropConfiguration? configurationSource;
@@ -25,7 +25,6 @@ public class SystemBackdrop
/// 构造一个新的系统背景帮助类
/// </summary>
/// <param name="window">窗体</param>
/// <param name="fallBackBehavior">回退行为</param>
public SystemBackdrop(Window window)
{
this.window = window;
@@ -49,7 +48,7 @@ public class SystemBackdrop
}
else
{
dispatcherQueueHelper = new WindowsSystemDispatcherQueueHelper();
dispatcherQueueHelper = new DispatcherQueueHelper();
dispatcherQueueHelper.EnsureWindowsSystemDispatcherQueueController();
// Hooking up the policy object
@@ -104,16 +103,16 @@ public class SystemBackdrop
{
Must.NotNull(configurationSource!).Theme = ((FrameworkElement)window.Content).ActualTheme switch
{
ElementTheme.Dark => SystemBackdropTheme.Dark,
ElementTheme.Light => SystemBackdropTheme.Light,
ElementTheme.Default => SystemBackdropTheme.Default,
ElementTheme.Light => SystemBackdropTheme.Light,
ElementTheme.Dark => SystemBackdropTheme.Dark,
_ => throw Must.NeverHappen(),
};
}
private class WindowsSystemDispatcherQueueHelper
private class DispatcherQueueHelper
{
private object dispatcherQueueController = null!;
private object? dispatcherQueueController = null;
/// <summary>
/// 确保系统调度队列控制器存在
@@ -128,19 +127,21 @@ public class SystemBackdrop
if (dispatcherQueueController == null)
{
DispatcherQueueOptions options;
options.DwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions));
options.ThreadType = 2; // DQTYPE_THREAD_CURRENT
options.ApartmentType = 2; // DQTAT_COM_STA
DispatcherQueueOptions options = new()
{
DwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions)),
ThreadType = 2, // DQTYPE_THREAD_CURRENT
ApartmentType = 2, // DQTAT_COM_STA
};
_ = CreateDispatcherQueueController(options, ref dispatcherQueueController!);
_ = CreateDispatcherQueueController(options, ref dispatcherQueueController);
}
}
[DllImport("CoreMessaging.dll")]
private static extern int CreateDispatcherQueueController(
[In] DispatcherQueueOptions options,
[In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object dispatcherQueueController);
[In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object? dispatcherQueueController);
[StructLayout(LayoutKind.Sequential)]
private struct DispatcherQueueOptions

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control.Behavior;
/// <summary>
/// 在元素加载完成后执行命令的行为
/// </summary>
internal class InvokeCommandOnLoadedBehavior : BehaviorBase<UIElement>
{
private static readonly DependencyProperty CommandProperty = Property<InvokeCommandOnLoadedBehavior>.Depend<ICommand>(nameof(Command));
private static readonly DependencyProperty CommandParameterProperty = Property<InvokeCommandOnLoadedBehavior>.Depend<object>(nameof(CommandParameter));
/// <summary>
/// 待执行的命令
/// </summary>
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
/// <summary>
/// 命令参数
/// </summary>
[MaybeNull]
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
/// <inheritdoc/>
protected override void OnAssociatedObjectLoaded()
{
if (Command != null && Command.CanExecute(CommandParameter))
{
Command?.Execute(CommandParameter);
}
base.OnAssociatedObjectLoaded();
}
}

View File

@@ -1,10 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI;
using CommunityToolkit.WinUI.UI.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Extension;
namespace Snap.Hutao.Control.Image;
@@ -14,11 +14,15 @@ 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;
}
@@ -29,7 +33,7 @@ public class CachedImage : ImageEx
BitmapImage? image;
try
{
image = await ImageCache.Instance.GetFromCacheAsync(imageUri, true, token);
image = await imageCache.GetFromCacheAsync(imageUri, true, token);
}
catch (TaskCanceledException)
{
@@ -39,7 +43,7 @@ public class CachedImage : ImageEx
catch
{
// maybe the image is corrupted, remove it.
await ImageCache.Instance.RemoveAsync(imageUri.Enumerate());
await imageCache.RemoveAsync(imageUri.Enumerate());
throw;
}

View File

@@ -8,11 +8,11 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using System.Numerics;
using Windows.Graphics.Imaging;
using Windows.Storage;
@@ -26,7 +26,8 @@ namespace Snap.Hutao.Control.Image;
public class Gradient : Microsoft.UI.Xaml.Controls.Control
{
private static readonly DependencyProperty SourceProperty = Property<Gradient>.Depend(nameof(Source), string.Empty, OnSourceChanged);
private static readonly ConcurrentCancellationTokenSource<Gradient> ImageLoading = new();
private static readonly ConcurrentCancellationTokenSource<Gradient> LoadingTokenSource = new();
private SpriteVisual? spriteVisual;
private double imageAspectRatio;
@@ -47,31 +48,20 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control
set => SetValue(SourceProperty, value);
}
private static async Task<StorageFile> GetCachedFileAsync(string url, CancellationToken token)
{
string fileName = CacheContext.GetCacheFileName(url);
CacheContext cacheContext = Ioc.Default.GetRequiredService<CacheContext>();
StorageFile storageFile;
if (!cacheContext.FileExists(fileName))
{
storageFile = await CacheContext.Folder.CreateFileAsync(fileName).AsTask(token);
await StreamHelper.GetHttpStreamToStorageFileAsync(new(url), storageFile);
}
else
{
storageFile = await CacheContext.Folder.GetFileAsync(fileName).AsTask(token);
}
return storageFile;
}
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
{
Gradient gradient = (Gradient)sender;
string url = (string)arg.NewValue;
gradient.ApplyImageAsync(url, ImageLoading.Register(gradient)).SafeForget();
ILogger<Gradient> logger = Ioc.Default.GetRequiredService<ILogger<Gradient>>();
gradient.ApplyImageAsync(url, LoadingTokenSource.Register(gradient)).SafeForget(logger, OnApplyImageFailed);
}
private static void OnApplyImageFailed(Exception exception)
{
Ioc.Default
.GetRequiredService<IInfoBarService>()
.Error(exception, "应用渐变背景时发生异常");
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
@@ -98,7 +88,7 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control
{
await AnimationBuilder
.Create()
.Opacity(0, 1)
.Opacity(0d)
.StartAsync(this, token);
StorageFile storageFile = await GetCachedFileAsync(url, token);
@@ -114,6 +104,7 @@ 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);
@@ -123,10 +114,40 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control
await AnimationBuilder
.Create()
.Opacity(1, 0)
.Opacity(1d)
.StartAsync(this, token);
}
private async Task<StorageFile> GetCachedFileAsync(string url, CancellationToken token)
{
string fileName = CacheContext.GetCacheFileName(url);
CacheContext cacheContext = Ioc.Default.GetRequiredService<CacheContext>();
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;
}
private async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
@@ -137,4 +158,4 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control
return LoadedImageSurface.StartLoadFromStream(imageStream);
}
}
}
}

View File

@@ -24,21 +24,4 @@ public static class Browser
failAction?.Invoke(ex);
}
}
/// <summary>
/// 打开浏览器
/// </summary>
/// <param name="urlFunc">获取链接回调</param>
/// <param name="failAction">失败时执行的回调</param>
public static void Open(Func<string> urlFunc, Action<Exception>? failAction = null)
{
try
{
ProcessHelper.Start(urlFunc.Invoke());
}
catch (Exception ex)
{
failAction?.Invoke(ex);
}
}
}

View File

@@ -0,0 +1,538 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using Windows.Storage;
namespace Snap.Hutao.Core.Caching
{
/// <summary>
/// Provides methods and tools to cache files in a folder
/// </summary>
/// <typeparam name="T">Generic type as supplied by consumer of the class</typeparam>
public abstract class CacheBase<T>
{
private class ConcurrentRequest
{
public Task<T> Task { get; set; }
public bool EnsureCachedCopy { get; set; }
}
private readonly SemaphoreSlim _cacheFolderSemaphore = new(1);
private StorageFolder _baseFolder = null;
private string _cacheFolderName = null;
private StorageFolder _cacheFolder = null;
private InMemoryStorage<T> _inMemoryFileStorage = null;
private ConcurrentDictionary<string, ConcurrentRequest> _concurrentTasks = new ConcurrentDictionary<string, ConcurrentRequest>();
private HttpClient _httpClient = null;
/// <summary>
/// Initializes a new instance of the <see cref="CacheBase{T}"/> class.
/// </summary>
protected CacheBase()
{
CacheDuration = TimeSpan.FromDays(1);
_inMemoryFileStorage = new InMemoryStorage<T>();
RetryCount = 1;
}
/// <summary>
/// Gets or sets the life duration of every cache entry.
/// </summary>
public TimeSpan CacheDuration { get; set; }
/// <summary>
/// Gets or sets the number of retries trying to ensure the file is cached.
/// </summary>
public uint RetryCount { get; set; }
/// <summary>
/// Gets or sets max in-memory item storage count
/// </summary>
public int MaxMemoryCacheCount
{
get
{
return _inMemoryFileStorage.MaxItemCount;
}
set
{
_inMemoryFileStorage.MaxItemCount = value;
}
}
/// <summary>
/// Gets instance of <see cref="HttpClient"/>
/// </summary>
protected HttpClient HttpClient
{
get
{
if (_httpClient == null)
{
var messageHandler = new HttpClientHandler() { MaxConnectionsPerServer = 20 };
_httpClient = new HttpClient(messageHandler);
}
return _httpClient;
}
}
/// <summary>
/// Initializes FileCache and provides root folder and cache folder name
/// </summary>
/// <param name="folder">Folder that is used as root for cache</param>
/// <param name="folderName">Cache folder name</param>
/// <param name="httpMessageHandler">instance of <see cref="HttpMessageHandler"/></param>
/// <returns>awaitable task</returns>
public virtual async Task InitializeAsync(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);
}
}
/// <summary>
/// Clears all files in the cache
/// </summary>
/// <returns>awaitable task</returns>
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();
}
/// <summary>
/// Clears file if it has expired
/// </summary>
/// <param name="duration">timespan to compute whether file has expired or not</param>
/// <returns>awaitable task</returns>
public Task ClearAsync(TimeSpan duration)
{
return RemoveExpiredAsync(duration);
}
/// <summary>
/// Removes cached files that have expired
/// </summary>
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
/// <returns>awaitable task</returns>
public async Task RemoveExpiredAsync(TimeSpan? duration = null)
{
TimeSpan expiryDuration = duration ?? CacheDuration;
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
var filesToDelete = new List<StorageFile>();
foreach (var file in files)
{
if (file == null)
{
continue;
}
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false))
{
filesToDelete.Add(file);
}
}
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
_inMemoryFileStorage.Clear(expiryDuration);
}
/// <summary>
/// Removed items based on uri list passed
/// </summary>
/// <param name="uriForCachedItems">Enumerable uri list</param>
/// <returns>awaitable Task</returns>
public async Task RemoveAsync(IEnumerable<Uri> uriForCachedItems)
{
if (uriForCachedItems == null || !uriForCachedItems.Any())
{
return;
}
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
var filesToDelete = new List<StorageFile>();
var keys = new List<string>();
Dictionary<string, StorageFile> hashDictionary = new Dictionary<string, StorageFile>();
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);
}
/// <summary>
/// Assures that item represented by Uri is cached.
/// </summary>
/// <param name="uri">Uri of the item</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be cached</param>
/// <param name="storeToMemoryCache">Indicates if item should be loaded into the in-memory storage</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <returns>Awaitable Task</returns>
public Task PreCacheAsync(Uri uri, bool throwOnError = false, bool storeToMemoryCache = false, CancellationToken cancellationToken = default(CancellationToken))
{
return GetItemAsync(uri, throwOnError, !storeToMemoryCache, cancellationToken, null);
}
/// <summary>
/// Retrieves item represented by Uri from the cache. If the item is not found in the cache, it will try to downloaded and saved before returning it to the caller.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be found / downloaded.</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <param name="initializerKeyValues">key value pairs used when initializing instance of generic type</param>
/// <returns>an instance of Generic type</returns>
public Task<T> GetFromCacheAsync(Uri uri, bool throwOnError = false, CancellationToken cancellationToken = default(CancellationToken), List<KeyValuePair<string, object>> initializerKeyValues = null)
{
return GetItemAsync(uri, throwOnError, false, cancellationToken, initializerKeyValues);
}
/// <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)
{
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
string fileName = GetCacheFileName(uri);
var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
return item as StorageFile;
}
/// <summary>
/// Retrieves item represented by Uri from the in-memory cache if it exists and is not out of date. If item is not found or is out of date, default instance of the generic type is returned.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>an instance of Generic type</returns>
public T GetFromMemoryCache(Uri uri)
{
T instance = default(T);
string fileName = GetCacheFileName(uri);
if (_inMemoryFileStorage.MaxItemCount > 0)
{
var msi = _inMemoryFileStorage.GetItem(fileName, CacheDuration);
if (msi != null)
{
instance = msi.Item;
}
}
return instance;
}
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="stream">input stream</param>
/// <param name="initializerKeyValues">key value pairs used when initializing instance of generic type</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> InitializeTypeAsync(Stream stream, List<KeyValuePair<string, object>> initializerKeyValues = null);
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="baseFile">storage file</param>
/// <param name="initializerKeyValues">key value pairs used when initializing instance of generic type</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> InitializeTypeAsync(StorageFile baseFile, List<KeyValuePair<string, object>> initializerKeyValues = null);
/// <summary>
/// Override-able method that checks whether file is valid or not.
/// </summary>
/// <param name="file">storage file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected virtual async Task<bool> IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
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++)
{
value += (ulong)utf8[n] << ((n * 5) % 56);
}
return value;
}
private async Task<T> GetItemAsync(Uri uri, bool throwOnError, bool preCacheOnly, CancellationToken cancellationToken, List<KeyValuePair<string, object>> initializerKeyValues)
{
T instance = default(T);
string fileName = GetCacheFileName(uri);
_concurrentTasks.TryGetValue(fileName, out var request);
// if similar request exists check if it was preCacheOnly and validate that current request isn't preCacheOnly
if (request != null && request.EnsureCachedCopy && !preCacheOnly)
{
await request.Task.ConfigureAwait(false);
request = null;
}
if (request == null)
{
request = new ConcurrentRequest()
{
Task = GetFromCacheOrDownloadAsync(uri, fileName, preCacheOnly, cancellationToken, initializerKeyValues),
EnsureCachedCopy = preCacheOnly
};
_concurrentTasks[fileName] = request;
}
try
{
instance = await request.Task.ConfigureAwait(false);
}
catch (Exception ex)
{
global::System.Diagnostics.Debug.WriteLine(ex.Message);
if (throwOnError)
{
throw;
}
}
finally
{
_concurrentTasks.TryRemove(fileName, out _);
}
return instance;
}
private async Task<T> GetFromCacheOrDownloadAsync(Uri uri, string fileName, bool preCacheOnly, CancellationToken cancellationToken, List<KeyValuePair<string, object>> 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
{
instance = await DownloadFileAsync(uri, baseFile, preCacheOnly, cancellationToken, initializerKeyValues).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)) && !preCacheOnly)
{
instance = await InitializeTypeAsync(baseFile, initializerKeyValues).ConfigureAwait(false);
if (_inMemoryFileStorage.MaxItemCount > 0)
{
var properties = await baseFile.GetBasicPropertiesAsync().AsTask().ConfigureAwait(false);
var msi = new InMemoryStorageItem<T>(fileName, properties.DateModified.DateTime, instance);
_inMemoryFileStorage.SetItem(msi);
}
}
return instance;
}
private async Task<T> DownloadFileAsync(Uri uri, StorageFile baseFile, bool preCacheOnly, CancellationToken cancellationToken, List<KeyValuePair<string, object>> 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())
{
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);
}
}
return instance;
}
private async Task InternalClearAsync(IEnumerable<StorageFile> files)
{
foreach (var file in files)
{
try
{
await file.DeleteAsync().AsTask().ConfigureAwait(false);
}
catch
{
// Just ignore errors for now}
}
}
}
/// <summary>
/// Initializes with default values if user has not initialized explicitly
/// </summary>
/// <returns>awaitable task</returns>
private async Task ForceInitialiseAsync()
{
if (_cacheFolder != null)
{
return;
}
await _cacheFolderSemaphore.WaitAsync().ConfigureAwait(false);
_inMemoryFileStorage = new InMemoryStorage<T>();
if (_baseFolder == null)
{
_baseFolder = 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<StorageFolder> GetCacheFolderAsync()
{
if (_cacheFolder == null)
{
await ForceInitialiseAsync().ConfigureAwait(false);
}
return _cacheFolder;
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Media.Imaging;
using System.Collections.Generic;
namespace Snap.Hutao.Core.Caching;
/// <summary>
/// 为图像缓存提供抽象
/// </summary>
/// <typeparam name="T">缓存类型</typeparam>
internal interface IImageCache
{
/// <summary>
/// Retrieves item represented by Uri from the cache. If the item is not found in the cache, it will try to downloaded and saved before returning it to the caller.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be found / downloaded.</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <param name="initializerKeyValues">key value pairs used when initializing instance of generic type</param>
/// <returns>an instance of Generic type</returns>
[SuppressMessage("", "CA1068")]
Task<BitmapImage> GetFromCacheAsync(Uri uri, bool throwOnError = false, CancellationToken cancellationToken = default(CancellationToken), List<KeyValuePair<string, object>> initializerKeyValues = null!);
/// <summary>
/// Removed items based on uri list passed
/// </summary>
/// <param name="uriForCachedItems">Enumerable uri list</param>
/// <returns>awaitable Task</returns>
Task RemoveAsync(IEnumerable<Uri> uriForCachedItems);
}

View File

@@ -0,0 +1,143 @@
// Copyright (c) DGP Studio. All rights reserved.
// 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 Windows.Storage;
using Windows.Storage.FileProperties;
namespace Snap.Hutao.Core.Caching;
/// <summary>
/// Provides methods and tools to cache files in a folder
/// The class's name will become the cache folder's name
/// </summary>
[Injection(InjectAs.Singleton, typeof(IImageCache))]
public class ImageCache : CacheBase<BitmapImage>, IImageCache
{
private const string DateAccessedProperty = "System.DateAccessed";
private readonly List<string> extendedPropertyNames = new()
{
DateAccessedProperty,
};
/// <summary>
/// Initializes a new instance of the <see cref="ImageCache"/> class.
/// </summary>
/// <param name="httpClient">http客户端</param>
public ImageCache()
{
DispatcherQueue = Program.UIDispatcherQueue;
CacheDuration = TimeSpan.FromDays(30);
RetryCount = 3;
}
/// <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>
/// <param name="initializerKeyValues">key value pairs used when initializing instance of generic type</param>
/// <returns>awaitable task</returns>
protected override Task<BitmapImage> InitializeTypeAsync(Stream stream, List<KeyValuePair<string, object>> initializerKeyValues = null!)
{
if (stream.Length == 0)
{
throw new FileNotFoundException();
}
return DispatcherQueue.EnqueueAsync(async () =>
{
BitmapImage image = new();
if (initializerKeyValues != null && initializerKeyValues.Count > 0)
{
foreach (KeyValuePair<string, object> 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);
return image;
});
}
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="baseFile">storage file</param>
/// <param name="initializerKeyValues">key value pairs used when initializing instance of generic type</param>
/// <returns>awaitable task</returns>
protected override async Task<BitmapImage> InitializeTypeAsync(StorageFile baseFile, List<KeyValuePair<string, object>> initializerKeyValues = null!)
{
using (Stream stream = await baseFile.OpenStreamForReadAsync().ConfigureAwait(false))
{
return await InitializeTypeAsync(stream, initializerKeyValues).ConfigureAwait(false);
}
}
/// <summary>
/// Override-able method that checks whether file is valid or not.
/// </summary>
/// <param name="file">storage file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected override async Task<bool> IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
// Get extended properties.
IDictionary<string, object> extraProperties = await file.Properties
.RetrievePropertiesAsync(extendedPropertyNames)
.AsTask()
.ConfigureAwait(false);
// Get date-accessed property.
object? propValue = extraProperties[DateAccessedProperty];
if (propValue != null)
{
DateTimeOffset? lastAccess = propValue as DateTimeOffset?;
if (lastAccess.HasValue)
{
return DateTime.Now.Subtract(lastAccess.Value.DateTime) > duration;
}
}
BasicProperties properties = await file
.GetBasicPropertiesAsync()
.AsTask()
.ConfigureAwait(false);
return properties.Size == 0 || DateTime.Now.Subtract(properties.DateModified.DateTime) > duration;
}
}

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core;
/// <summary>
/// 核心环境参数
/// </summary>
internal static class CoreEnvironment
internal static partial class CoreEnvironment
{
/// <summary>
/// 当前版本
@@ -43,3 +43,8 @@ internal static class CoreEnvironment
return Md5Convert.ToHexString($"{userName}{machineGuid}");
}
}
internal static partial class CoreEnvironment
{
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Core.Diagnostics;
/// <summary>
/// 值类型的<see cref="Stopwatch"/>
/// </summary>
internal struct ValueStopwatch
{
private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
private readonly long startTimestamp;
private ValueStopwatch(long startTimestamp)
{
this.startTimestamp = startTimestamp;
}
/// <summary>
/// 是否处于活动状态
/// </summary>
public bool IsActive => startTimestamp != 0;
/// <summary>
/// 触发一个新的停表
/// </summary>
/// <returns>一个新的停表实例</returns>
public static ValueStopwatch StartNew() => new(Stopwatch.GetTimestamp());
/// <summary>
/// 获取经过的时间
/// </summary>
/// <returns>经过的时间</returns>
/// <exception cref="InvalidOperationException">当前的停表未合理的初始化</exception>
public TimeSpan GetElapsedTime()
{
// Start timestamp can't be zero in an initialized ValueStopwatch.
// It would have to be literally the first thing executed when the machine boots to be 0.
// So it being 0 is a clear indication of default(ValueStopwatch)
Verify.Operation(IsActive, $"An uninitialized, or 'default', {nameof(ValueStopwatch)} cannot be used to get elapsed time.");
long end = Stopwatch.GetTimestamp();
long timestampDelta = end - startTimestamp;
long ticks = (long)(TimestampToTicks * timestampDelta);
return new TimeSpan(ticks);
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// Extension methods for the <see cref="ILoggerFactory"/> class.
/// </summary>
public static class DatabaseLoggerFactoryExtensions
{
/// <summary>
/// Adds a debug logger named 'Debug' to the factory.
/// </summary>
/// <param name="builder">The extension method argument.</param>
/// <returns>日志构造器</returns>
public static ILoggingBuilder AddDatabase(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DatebaseLoggerProvider>());
return builder;
}
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Context.Database;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// A logger that writes messages in the database table
/// </summary>
internal sealed partial class DatebaseLogger : ILogger
{
private readonly string name;
private readonly LogDbContext logDbContext;
private readonly object logDbContextLock;
/// <summary>
/// Initializes a new instance of the <see cref="DatebaseLogger"/> class.
/// </summary>
/// <param name="name">The name of the logger.</param>
/// <param name="logDbContext">应用程序数据库上下文</param>
/// <param name="logDbContextLock">上下文锁</param>
public DatebaseLogger(string name, LogDbContext logDbContext, object logDbContextLock)
{
this.name = name;
this.logDbContext = logDbContext;
this.logDbContextLock = logDbContextLock;
}
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state)
{
return new NullScope();
}
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
// If the filter is null, everything is enabled
return logLevel != LogLevel.None;
}
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
string message = Must.NotNull(formatter)(state, exception);
if (string.IsNullOrEmpty(message))
{
return;
}
lock (logDbContextLock)
{
logDbContext.Logs.Add(new LogEntry
{
Category = name,
LogLevel = logLevel,
EventId = eventId.Id,
Message = message,
Exception = exception?.ToString(),
});
logDbContext.SaveChanges();
}
}
/// <summary>
/// An empty scope without any logic
/// </summary>
private struct NullScope : IDisposable
{
public NullScope()
{
}
/// <inheritdoc />
public void Dispose()
{
}
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Context.FileSystem;
using System.Diagnostics;
using System.Linq;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// The provider for the <see cref="DebugLogger"/>.
/// </summary>
[ProviderAlias("Database")]
public class DatebaseLoggerProvider : ILoggerProvider
{
// the provider is created per logger, we don't want to create to much
private static volatile LogDbContext? logDbContext;
private static readonly object logDbContextLock = new();
private static LogDbContext LogDbContext
{
get
{
if (logDbContext == null)
{
lock (logDbContextLock)
{
// prevent re-entry call
if (logDbContext == null)
{
MyDocumentContext myDocument = new(new());
logDbContext = LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}");
if (logDbContext.Database.GetPendingMigrations().Any())
{
Debug.WriteLine("Performing LogDbContext Migrations");
logDbContext.Database.Migrate();
}
logDbContext.Logs.RemoveRange(logDbContext.Logs);
logDbContext.SaveChanges();
}
}
}
return logDbContext;
}
}
/// <inheritdoc/>
public ILogger CreateLogger(string name)
{
return new DatebaseLogger(name, LogDbContext, logDbContextLock);
}
/// <inheritdoc/>
public void Dispose()
{
}
}

View File

@@ -8,23 +8,59 @@ namespace Snap.Hutao.Core.Logging;
/// </summary>
internal static class EventIds
{
// 异常
/// <summary>
/// 未经处理的异常
/// </summary>
public static readonly EventId UnhandledException = 100000;
/// <summary>
/// Forget任务执行异常
/// </summary>
public static readonly EventId TaskException = 100001;
/// <summary>
/// 异步命令执行异常
/// </summary>
public static readonly EventId AsyncCommandException = 100002;
/// <summary>
/// WebView2环境异常
/// </summary>
public static readonly EventId WebView2EnvironmentException = 100003;
// 服务
/// <summary>
/// 导航历史
/// </summary>
public static readonly EventId NavigationHistory = 100100;
/// <summary>
/// 导航失败
/// </summary>
public static readonly EventId NavigationFailed = new(100000, nameof(NavigationFailed));
public static readonly EventId NavigationFailed = 100101;
/// <summary>
/// 未处理的异常
/// 元数据初始化过程
/// </summary>
public static readonly EventId UnhandledException = new(100001, nameof(UnhandledException));
public static readonly EventId MetadataInitialization = 100110;
/// <summary>
/// Forget任务执行异常
/// 元数据文件MD5检查
/// </summary>
public static readonly EventId TaskException = new(100002, nameof(TaskException));
public static readonly EventId MetadataFileMD5Check = 100111;
// 杂项
/// <summary>
/// Forget任务执行异常
/// 杂项Log
/// </summary>
public static readonly EventId AsyncCommandException = new(100003, nameof(AsyncCommandException));
public static readonly EventId CommonLog = 200000;
/// <summary>
/// 背景状态
/// </summary>
public static readonly EventId BackdropState = 200001;
}

View File

@@ -0,0 +1,47 @@
// 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;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// 数据库日志入口点
/// </summary>
[Table("logs")]
public class LogEntry
{
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 类别
/// </summary>
public string Category { get; set; } = default!;
/// <summary>
/// 日志等级
/// </summary>
public LogLevel LogLevel { get; set; }
/// <summary>
/// 事件Id
/// </summary>
public int EventId { get; set; }
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; } = default!;
/// <summary>
/// 可能的异常
/// </summary>
public string? Exception { get; set; }
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core.Logging;
namespace Snap.Hutao.Core;
@@ -36,7 +37,8 @@ internal abstract class WebView2Helper
}
catch (Exception ex)
{
Ioc.Default.GetRequiredService<ILogger<WebView2Helper>>().LogError(ex, "WebView2 运行时未安装");
ILogger<WebView2Helper> logger = Ioc.Default.GetRequiredService<ILogger<WebView2Helper>>();
logger.LogError(EventIds.WebView2EnvironmentException, ex, "WebView2 运行时未安装");
isSupported = false;
}

View File

@@ -60,7 +60,9 @@ internal static class CompositionExtensions
/// <param name="compositor">合成器</param>
/// <param name="sourceBrush">源</param>
/// <returns>合成效果画刷</returns>
public static CompositionEffectBrush CompositeLuminanceToAlphaEffectBrush(this Compositor compositor, CompositionBrush sourceBrush)
public static CompositionEffectBrush CompositeLuminanceToAlphaEffectBrush(
this Compositor compositor,
CompositionBrush sourceBrush)
{
LuminanceToAlphaEffect effect = new()
{
@@ -150,5 +152,24 @@ internal static class CompositionExtensions
return brush;
}
/// <summary>
/// 创建一个新的蒙版画刷
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="source">源</param>
/// <param name="mask">蒙版</param>
/// <returns>蒙版画刷</returns>
public static CompositionMaskBrush CompositeMaskBrush(
this Compositor compositor,
CompositionBrush source,
CompositionBrush mask)
{
CompositionMaskBrush brush = compositor.CreateMaskBrush();
brush.Source = source;
brush.Mask = mask;
return brush;
}
public record struct GradientStop(float Offset, Windows.UI.Color Color);
}

View File

@@ -9,19 +9,8 @@ namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="IEnumerable{T}"/> 扩展
/// </summary>
public static class EnumerableExtensions
public static partial class EnumerableExtensions
{
/// <summary>
/// 将源转换为仅包含单个元素的枚举
/// </summary>
/// <typeparam name="TSource">源的类型</typeparam>
/// <param name="source">源</param>
/// <returns>集合</returns>
public static IEnumerable<TSource> Enumerate<TSource>(this TSource source)
{
yield return source;
}
/// <summary>
/// 计数
/// </summary>
@@ -66,6 +55,17 @@ public static class EnumerableExtensions
return source ?? new();
}
/// <summary>
/// 将源转换为仅包含单个元素的枚举
/// </summary>
/// <typeparam name="TSource">源的类型</typeparam>
/// <param name="source">源</param>
/// <returns>集合</returns>
public static IEnumerable<TSource> Enumerate<TSource>(this TSource source)
{
yield return source;
}
/// <summary>
/// 寻找枚举中唯一的值,找不到时
/// 回退到首个或默认值

View File

@@ -1,21 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
/// <summary>
/// 内存缓存扩展
/// </summary>
public static class MemoryCacheExtensions
{
/// <summary>
/// 获取缓存键名称
/// </summary>
/// <param name="className">类名</param>
/// <param name="propertyName">属性名</param>
/// <returns>缓存</returns>
public static string GetCacheKey(string className, string propertyName)
{
return $"{className}.Cache.{propertyName}";
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
/// <summary>
/// 对象扩展
/// </summary>
public static class ObjectExtensions
{
/// <summary>
/// <see langword="as"/> 的链式调用扩展
/// </summary>
/// <typeparam name="T">目标转换类型</typeparam>
/// <param name="obj">对象</param>
/// <returns>转换类型后的对象</returns>
public static T? As<T>(this object? obj)
where T : class
{
return obj as T;
}
}

View File

@@ -60,4 +60,16 @@ internal static class ReflectionExtension
action.Invoke(type);
}
}
/// <summary>
/// 按字段名称设置值
/// </summary>
/// <param name="obj">对象</param>
/// <param name="fieldName">字段名称</param>
/// <param name="value">值</param>
public static void SetPrivateFieldValueByName(this object obj, string fieldName, object? value)
{
FieldInfo? fieldInfo = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
fieldInfo?.SetValue(obj, value);
}
}

View File

@@ -8,28 +8,90 @@ namespace Snap.Hutao.Extension;
/// <summary>
/// 任务扩展
/// </summary>
[SuppressMessage("", "VSTHRD003")]
[SuppressMessage("", "VSTHRD100")]
public static class TaskExtensions
{
/// <summary>
/// 安全的触发任务
/// </summary>
/// <param name="task">任务</param>
/// <param name="continueOnCapturedContext">是否在捕获的上下文中继续执行</param>
/// <param name="logger">日志器</param>
[SuppressMessage("", "VSTHRD003")]
[SuppressMessage("", "VSTHRD100")]
public static async void SafeForget(this Task task, bool continueOnCapturedContext = true, ILogger? logger = null)
public static async void SafeForget(this Task task)
{
try
{
await task.ConfigureAwait(continueOnCapturedContext);
await task;
}
catch
{
}
}
/// <summary>
/// 安全的触发任务
/// </summary>
/// <param name="task">任务</param>
/// <param name="logger">日志器</param>
public static async void SafeForget(this Task task, ILogger? logger = null)
{
try
{
await task;
}
catch (TaskCanceledException)
{
// Do nothing
}
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:{exception}", nameof(SafeForget), e.GetBaseException());
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
}
}
/// <summary>
/// 安全的触发任务
/// </summary>
/// <param name="task">任务</param>
/// <param name="logger">日志器</param>
/// <param name="onException">发生异常时调用</param>
public static async void SafeForget(this Task task, ILogger? logger = null, Action<Exception>? onException = null)
{
try
{
await task;
}
catch (TaskCanceledException)
{
// Do nothing
}
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
onException?.Invoke(e);
}
}
/// <summary>
/// 安全的触发任务
/// </summary>
/// <param name="task">任务</param>
/// <param name="logger">日志器</param>
/// <param name="onCanceled">任务取消时调用</param>
/// <param name="onException">发生异常时调用</param>
public static async void SafeForget(this Task task, ILogger? logger = null, Action? onCanceled = null, Action<Exception>? onException = null)
{
try
{
await task;
}
catch (TaskCanceledException)
{
onCanceled?.Invoke();
}
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
onException?.Invoke(e);
}
}
}

View File

@@ -22,4 +22,4 @@ public static class TupleExtensions
{
return new Dictionary<TKey, TValue>(1) { { tuple.Key, tuple.Value } };
}
}
}

View File

@@ -93,7 +93,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
if (asyncRelayCommand.ExecutionTask?.Exception is AggregateException exception)
{
Exception baseException = exception.GetBaseException();
logger.LogError(EventIds.AsyncCommandException, baseException, "异步命令发生了错误");
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(asyncRelayCommand));
}
}
}

View File

@@ -51,7 +51,7 @@ internal static class IocConfiguration
{
if (context.Database.GetPendingMigrations().Any())
{
Debug.WriteLine("Performing Migrations");
Debug.WriteLine("Performing AppDbContext Migrations");
context.Database.Migrate();
}
}

View File

@@ -2,6 +2,7 @@
// 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;
@@ -29,6 +30,7 @@ internal static class IocHttpClientConfiguration
{
// services
services.AddHttpClient<MetadataService>(DefaultConfiguration);
// services.AddHttpClient<ImageCache>(DefaultConfiguration);
// normal clients
services.AddHttpClient<AnnouncementClient>(DefaultConfiguration);

View File

@@ -4,9 +4,9 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Control.HostBackdrop;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Win32;
using System.Drawing;
using System.Runtime.InteropServices;
using WinRT.Interop;
@@ -47,32 +47,7 @@ public sealed partial class MainWindow : Window
return new RECT(left, top, right, bottom);
}
private void InitializeWindow()
{
ExtendsContentIntoTitleBar = true;
SetTitleBar(TitleBarView.DragableArea);
RECT rect = RetriveWindowRect();
if (!rect.Size.IsEmpty)
{
WINDOWPLACEMENT windowPlacement = new()
{
Length = Marshal.SizeOf<WINDOWPLACEMENT>(),
MaxPosition = new Point(-1, -1),
NormalPosition = rect,
ShowCmd = ShowWindowCommand.Normal,
};
User32.SetWindowPlacement(handle, ref windowPlacement);
}
User32.SetWindowText(handle, "胡桃");
bool micaApplied = new SystemBackdrop(this).TrySetBackdrop();
logger.LogInformation("{name} 设置{result}", nameof(SystemBackdrop), micaApplied ? "成功" : "失败");
}
private void SaveWindowRect()
private static void SaveWindowRect(IntPtr handle)
{
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Default;
User32.GetWindowPlacement(handle, ref windowPlacement);
@@ -83,12 +58,36 @@ public sealed partial class MainWindow : Window
LocalSetting.SetValueType(SettingKeys.WindowBottom, windowPlacement.NormalPosition.Bottom);
}
private void InitializeWindow()
{
ExtendsContentIntoTitleBar = true;
SetTitleBar(TitleBarView.DragableArea);
User32.SetWindowText(handle, "胡桃");
RECT rect = RetriveWindowRect();
if (!rect.Size.IsEmpty)
{
WINDOWPLACEMENT windowPlacement = new()
{
Length = Marshal.SizeOf<WINDOWPLACEMENT>(),
MaxPosition = new POINT(-1, -1),
NormalPosition = rect,
ShowCmd = ShowWindowCommand.Normal,
};
User32.SetWindowPlacement(handle, ref windowPlacement);
}
bool micaApplied = new SystemBackdrop(this).TrySetBackdrop();
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
}
private void MainWindowClosed(object sender, WindowEventArgs args)
{
SaveWindowRect();
SaveWindowRect(handle);
// save datebase
int changes = appDbContext.SaveChanges();
Verify.Operation(changes == 0, "存在可避免的未经处理的数据库更改");
Verify.Operation(changes == 0, "存在未经处理的数据库记录更改");
}
}

View File

@@ -11,13 +11,13 @@ using Snap.Hutao.Context.Database;
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20220618110357_SettingAndUser")]
partial class SettingAndUser
[Migration("20220720121642_Init")]
partial class Init
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.6");
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{

View File

@@ -1,16 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
[SuppressMessage("", "SA1413")]
[SuppressMessage("", "SA1600")]
[SuppressMessage("", "SA1601")]
public partial class SettingAndUser : Migration
public partial class Init : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.6");
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{

View File

@@ -0,0 +1,52 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Context.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
[Migration("20220720121521_Logs")]
partial class Logs
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,35 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
public partial class Logs : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "logs",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
Category = table.Column<string>(type: "TEXT", nullable: false),
LogLevel = table.Column<int>(type: "INTEGER", nullable: false),
EventId = table.Column<int>(type: "INTEGER", nullable: false),
Message = table.Column<string>(type: "TEXT", nullable: false),
Exception = table.Column<string>(type: "TEXT", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_logs", x => x.InnerId);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "logs");
}
}
}

View File

@@ -0,0 +1,48 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Snap.Hutao.Context.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
partial class LogDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -21,10 +21,19 @@ internal class AvatarNameCardPicConverter : IValueConverter
}
Avatar.Avatar avatar = (Avatar.Avatar)value;
string avatarName = avatar.Icon[14..];
string avatarName = ReplaceSpecialCaseNaming(avatar.Icon[14..]);
return new Uri(string.Format(BaseUrl, avatarName));
}
private static string ReplaceSpecialCaseNaming(string avatarName)
{
return avatarName switch
{
"Yae" => "Yae1",
_ => avatarName,
};
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{

View File

@@ -9,7 +9,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.0.13.0" />
Version="1.0.16.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -13,6 +13,13 @@ namespace Snap.Hutao;
/// </summary>
public static class Program
{
private static volatile DispatcherQueue? dispatcherQueue;
/// <summary>
/// 主线程调度器队列
/// </summary>
public static DispatcherQueue UIDispatcherQueue => Must.NotNull(dispatcherQueue!);
[DllImport("Microsoft.ui.xaml.dll")]
private static extern void XamlCheckProcessRequirements();
@@ -24,7 +31,8 @@ public static class Program
Application.Start(p =>
{
SynchronizationContext context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
dispatcherQueue = DispatcherQueue.GetForCurrentThread();
SynchronizationContext context = new DispatcherQueueSynchronizationContext(dispatcherQueue);
SynchronizationContext.SetSynchronizationContext(context);
_ = new App();
});

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using System.Collections.Generic;
@@ -15,7 +14,7 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Transient, typeof(IAnnouncementService))]
internal class AnnouncementService : IAnnouncementService
{
private static readonly string CacheKey = MemoryCacheExtensions.GetCacheKey(nameof(AnnouncementService), nameof(AnnouncementWrapper));
private static readonly string CacheKey = $"{nameof(AnnouncementService)}.Cache.{nameof(AnnouncementWrapper)}";
private readonly AnnouncementClient announcementClient;
private readonly IMemoryCache memoryCache;

View File

@@ -4,6 +4,8 @@
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;
@@ -78,13 +80,14 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
/// <inheritdoc/>
public async Task InitializeInternalAsync(CancellationToken token = default)
{
logger.LogInformation("元数据初始化开始");
logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion begin");
IsInitialized = await TryUpdateMetadataAsync(token)
.ConfigureAwait(false);
initializeCompletionSource.SetResult();
logger.LogInformation("元数据初始化完成");
logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion completed");
}
/// <inheritdoc/>
@@ -162,12 +165,11 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
/// <param name="metaMd5Map">元数据校验表</param>
/// <param name="token">取消令牌</param>
/// <returns>令牌</returns>
private async Task CheckMetadataAsync(IDictionary<string, string> metaMd5Map, CancellationToken token)
private Task CheckMetadataAsync(IDictionary<string, string> metaMd5Map, CancellationToken token)
{
// TODO: Make this foreach async to imporve speed
// enumerate files and compare md5
foreach ((string fileName, string md5) in metaMd5Map)
return Parallel.ForEachAsync(metaMd5Map, token, async (pair, token) =>
{
(string fileName, string md5) = pair;
string fileFullName = $"{fileName}.json";
bool skip = false;
@@ -179,12 +181,12 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
if (!skip)
{
logger.LogInformation("{file} 文件 MD5 不匹配", fileFullName);
logger.LogInformation(EventIds.MetadataFileMD5Check, "MD5 of {file} not matched", fileFullName);
await DownloadMetadataAsync(fileFullName, token)
.ConfigureAwait(false);
}
}
});
}
private async Task<string> GetFileMd5Async(string fileFullName, CancellationToken token)
@@ -212,15 +214,13 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
{
while (await streamReader.ReadLineAsync().ConfigureAwait(false) is string line)
{
await (streamReader.EndOfStream
? streamWriter.WriteAsync(line) // Don't append the last line
: streamWriter.WriteLineAsync(line))
.ConfigureAwait(false);
Func<string?, Task> writeMethod = streamReader.EndOfStream ? streamWriter.WriteAsync : streamWriter.WriteLineAsync;
await writeMethod(line).ConfigureAwait(false);
}
}
}
logger.LogInformation("{file} 下载完成", fileFullName);
logger.LogInformation("Download {file} completed", fileFullName);
}
private async ValueTask<T> GetMetadataAsync<T>(string fileName, CancellationToken token)

View File

@@ -103,6 +103,7 @@ internal class NavigationService : INavigationService
if (currentType == pageType)
{
logger.LogInformation(EventIds.NavigationHistory, "Navigate to {pageType} : succeed, already in", pageType);
return NavigationResult.AlreadyNavigatedTo;
}
@@ -112,15 +113,14 @@ internal class NavigationService : INavigationService
try
{
navigated = Frame?.Navigate(pageType, data) ?? false;
logger.LogInformation(EventIds.NavigationHistory, "Navigate to {pageType} : {result}", pageType, navigated ? "succeed" : "failed");
}
catch (Exception ex)
{
logger.LogError(EventIds.NavigationFailed, ex, "导航到指定页面时发生了错误");
logger.LogError(EventIds.NavigationFailed, ex, "An error occurred while navigating to {pageType}", pageType);
infoBarService.Error(ex);
}
logger.LogInformation("Navigate to {pageType}:{result}", pageType, navigated ? "succeed" : "failed");
// 首次导航失败时使属性持续保存为false
HasEverNavigated = HasEverNavigated || navigated;
return navigated ? NavigationResult.Succeed : NavigationResult.Failed;
@@ -143,7 +143,9 @@ internal class NavigationService : INavigationService
{
try
{
await data.WaitForCompletionAsync().ConfigureAwait(false);
await data
.WaitForCompletionAsync()
.ConfigureAwait(false);
}
catch (AggregateException)
{

View File

@@ -90,6 +90,11 @@
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SettingsUI\SettingsUI.csproj" />
<ProjectReference Include="..\Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<!-- Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored -->
@@ -139,10 +144,6 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SettingsUI\SettingsUI.csproj" />
<ProjectReference Include="..\Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\WikiAvatarPage.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.View.Helper;
public sealed class NavHelper
{
private static readonly DependencyProperty NavigateToProperty = Property<NavHelper>.Attach<Type>("NavigateTo");
private static readonly DependencyProperty ExtraDataProperty = Property<NavHelper>.Attach<INavigationData>("ExtraData");
private static readonly DependencyProperty ExtraDataProperty = Property<NavHelper>.Attach<object>("ExtraData");
/// <summary>
/// 获取导航项的目标页面类型
@@ -41,9 +41,9 @@ public sealed class NavHelper
/// </summary>
/// <param name="item">待获取的导航项</param>
/// <returns>目标页面类型的额外数据</returns>
public static INavigationData? GetExtraData(NavigationViewItem? item)
public static object? GetExtraData(NavigationViewItem? item)
{
return item?.GetValue(ExtraDataProperty) as INavigationData;
return item?.GetValue(ExtraDataProperty);
}
/// <summary>
@@ -51,7 +51,7 @@ public sealed class NavHelper
/// </summary>
/// <param name="item">待设置的导航项</param>
/// <param name="value">新的目标页面类型</param>
public static void SetExtraData(NavigationViewItem item, INavigationData value)
public static void SetExtraData(NavigationViewItem item, object value)
{
item.SetValue(ExtraDataProperty, value);
}

View File

@@ -1,12 +1,13 @@
<UserControl
x:Class="Snap.Hutao.View.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helper="using:Snap.Hutao.View.Helper"
xmlns:page="using:Snap.Hutao.View.Page"
xmlns:view="using:Snap.Hutao.View"
xmlns:cwu="using:CommunityToolkit.WinUI.UI"
xmlns:shv="using:Snap.Hutao.View"
xmlns:shvh="using:Snap.Hutao.View.Helper"
xmlns:shvp="using:Snap.Hutao.View.Page"
mc:Ignorable="d">
<UserControl.Resources>
<Thickness x:Key="NavigationViewContentMargin">0,44,0,0</Thickness>
@@ -20,40 +21,31 @@
ExpandedModeThresholdWidth="16"
IsPaneOpen="True"
IsBackEnabled="{Binding ElementName=ContentFrame,Path=CanGoBack}">
<NavigationView.MenuItems>
<NavigationViewItem Content="活动" helper:NavHelper.NavigateTo="page:AnnouncementPage">
<NavigationViewItem.Icon>
<BitmapIcon
UriSource="ms-appx:///Resource/Icon/UI_BtnIcon_ActivityEntry.png"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="成就" helper:NavHelper.NavigateTo="page:AchievementPage">
<NavigationViewItem.Icon>
<BitmapIcon UriSource="ms-appx:///Resource/Icon/UI_Icon_Achievement.png"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="角色" helper:NavHelper.NavigateTo="page:WikiAvatarPage">
<NavigationViewItem.Icon>
<BitmapIcon UriSource="ms-appx:///Resource/Icon/UI_BagTabIcon_Avatar.png"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem
Content="活动"
shvh:NavHelper.NavigateTo="shvp:AnnouncementPage"
Icon="{cwu:BitmapIcon 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}"/>
<NavigationViewItem
Content="角色"
shvh:NavHelper.NavigateTo="shvp:WikiAvatarPage"
Icon="{cwu:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BagTabIcon_Avatar.png}"/>
</NavigationView.MenuItems>
<NavigationView.PaneFooter>
<view:UserView IsExpanded="{Binding ElementName=NavView,Path=IsPaneOpen}"/>
<shv:UserView IsExpanded="{Binding ElementName=NavView,Path=IsPaneOpen}"/>
</NavigationView.PaneFooter>
<Frame x:Name="ContentFrame">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition>
<DrillInNavigationTransitionInfo/>
</NavigationThemeTransition>
<NavigationThemeTransition/>
</TransitionCollection>
</Frame.ContentTransitions>
</Frame>

View File

@@ -1,20 +1,17 @@
<shcc:CancellablePage
x:Class="Snap.Hutao.View.Page.AchievementPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Snap.Hutao.View.Page"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
xmlns:shcc="using:Snap.Hutao.Control.Cancellable"
xmlns:settings="using:SettingsUI.Controls"
xmlns:sc="using:SettingsUI.Controls"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shcc="using:Snap.Hutao.Control.Cancellable"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Loaded">
<mxic:InvokeCommandAction Command="{Binding OpenUICommand}"/>
</mxic:EventTriggerBehavior>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Grid>
<ScrollViewer>
@@ -23,7 +20,7 @@
ItemsSource="{Binding AchievementsView}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<settings:Setting
<sc:Setting
Margin="0,12,0,0"
Header="{Binding Title}"
Description="{Binding Description}"/>

View File

@@ -5,6 +5,17 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<WebView2 x:Name="WebView"/>
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
ActualThemeChanged="PageActualThemeChanged">
<Page.Transitions>
<TransitionCollection>
<NavigationThemeTransition>
<DrillInNavigationTransitionInfo/>
</NavigationThemeTransition>
</TransitionCollection>
</Page.Transitions>
<WebView2
x:Name="WebView"
IsRightTapEnabled="False"
DefaultBackgroundColor="Transparent"/>
</Page>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.VisualStudio.Threading;
using Snap.Hutao.Core;
@@ -14,6 +15,18 @@ namespace Snap.Hutao.View.Page;
/// </summary>
public sealed partial class AnnouncementContentPage : Microsoft.UI.Xaml.Controls.Page
{
private const string LightColor1 = "color:rgba(255,255,255,1)";
private const string LightColor2 = "color:rgba(238,238,238,1)";
private const string LightColor3 = "color:rgba(204,204,204,1)";
private const string LightColor4 = "color:rgba(198,196,191,1)";
private const string LightColor5 = "color:rgba(170,170,170,1)";
private const string DarkColor1 = "color:rgba(0,0,0,1)";
private const string DarkColor2 = "color:rgba(17,17,17,1)";
private const string DarkColor3 = "color:rgba(51,51,51,1)";
private const string DarkColor4 = "color:rgba(57,59,64,1)";
private const string DarkColor5 = "color:rgba(85,85,85,1)";
// support click open browser.
private const string MihoyoSDKDefinition =
@"window.miHoYoGameJSSDK = {
@@ -49,7 +62,7 @@ openInWebview: function(url){ location.href = url }}";
await WebView.EnsureCoreWebView2Async();
await WebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(MihoyoSDKDefinition);
WebView.CoreWebView2.WebMessageReceived += (_, e) => Browser.Open(e.TryGetWebMessageAsString);
WebView.CoreWebView2.WebMessageReceived += (_, e) => Browser.Open(e.TryGetWebMessageAsString());
}
catch (Exception ex)
{
@@ -57,7 +70,39 @@ openInWebview: function(url){ location.href = url }}";
return;
}
WebView.NavigateToString(targetContent);
WebView.NavigateToString(ReplaceForeground(targetContent, ActualTheme));
data.NotifyNavigationCompleted();
}
private void PageActualThemeChanged(FrameworkElement sender, object args)
{
WebView.NavigateToString(ReplaceForeground(targetContent, ActualTheme));
}
private string? ReplaceForeground(string? rawContent, ElementTheme theme)
{
if (string.IsNullOrWhiteSpace(rawContent))
{
return rawContent;
}
bool isDarkMode = theme switch
{
ElementTheme.Default => App.Current.RequestedTheme == ApplicationTheme.Dark,
ElementTheme.Dark => true,
_ => false,
};
if (isDarkMode)
{
rawContent = rawContent
.Replace(DarkColor5, LightColor5)
.Replace(DarkColor4, LightColor4)
.Replace(DarkColor3, LightColor3)
.Replace(DarkColor2, LightColor2);
}
// wrap a default color body around
return $@"<body style=""{(isDarkMode ? LightColor1 : DarkColor1)}"">{rawContent}</body>";
}
}

View File

@@ -15,13 +15,12 @@
xmlns:shca="using:Snap.Hutao.Control.Animation"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shcc="using:Snap.Hutao.Control.Cancellable"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shvc="using:Snap.Hutao.View.Converter"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Loaded">
<mxic:InvokeCommandAction Command="{Binding OpenUICommand}"/>
</mxic:EventTriggerBehavior>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<shcc:CancellablePage.Resources>
<cwuconv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
@@ -75,11 +74,14 @@
<mxi:Interaction.Behaviors>
<shcb:AutoHeightBehavior TargetWidth="1080" TargetHeight="390"/>
</mxi:Interaction.Behaviors>
<Border.Background>
<shci:CachedImage
Stretch="UniformToFill"
Source="{Binding Banner}"/>
<!--<Border.Background>
<ImageBrush
ImageSource="{Binding Banner}"
Stretch="UniformToFill"/>
</Border.Background>
</Border.Background>-->
<cwua:Explicit.Animations>
<cwua:AnimationSet x:Name="ImageZoomInAnimation">
<shca:ImageZoomInAnimation/>

View File

@@ -1,11 +1,10 @@
<Page
x:Class="Snap.Hutao.View.Page.SettingPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Snap.Hutao.View.Page"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:SettingsUI.Controls"
xmlns:sc="using:SettingsUI.Controls"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
@@ -18,24 +17,24 @@
<StackPanel
Margin="32,0,24,0">
<controls:SettingsGroup Header="关于 胡桃">
<controls:SettingExpander>
<controls:SettingExpander.Header>
<controls:Setting
<sc:SettingsGroup Header="关于 胡桃">
<sc:SettingExpander>
<sc:SettingExpander.Header>
<sc:Setting
Icon="&#xE117;"
Header="检查更新"
Description="根本没有检查更新选项">
</controls:Setting>
</controls:SettingExpander.Header>
</sc:Setting>
</sc:SettingExpander.Header>
<InfoBar
IsClosable="False"
Severity="Informational"
Message="都说了没有了"
IsOpen="True"
CornerRadius="0,0,4,4"/>
</controls:SettingExpander>
</controls:SettingsGroup>
</sc:SettingExpander>
</sc:SettingsGroup>
</StackPanel>
</ScrollViewer>

View File

@@ -5,9 +5,10 @@
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:mxic="using:Microsoft.Xaml.Interactions.Core"
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"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shct="using:Snap.Hutao.Control.Text"
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
@@ -17,9 +18,7 @@
d:DataContext="{d:DesignInstance Type=shv:WikiAvatarViewModel}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Loaded">
<mxic:InvokeCommandAction Command="{Binding OpenUICommand}"/>
</mxic:EventTriggerBehavior>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
@@ -150,7 +149,7 @@
DisplayMode="Inline"
OpenPaneLength="200">
<SplitView.PaneBackground>
<SolidColorBrush Color="{StaticResource CardBackgroundFillColorSecondary}"/>
<SolidColorBrush Color="{ThemeResource CardBackgroundFillColorSecondary}"/>
</SplitView.PaneBackground>
<SplitView.Pane>
<ListView
@@ -184,8 +183,7 @@
<Grid>
<shci:Gradient
VerticalAlignment="Top"
Source="{Binding Selected,Converter={StaticResource AvatarNameCardPicConverter}}">
</shci:Gradient>
Source="{Binding Selected,Converter={StaticResource AvatarNameCardPicConverter}}"/>
<ScrollViewer>
<StackPanel Margin="0,0,16,16">
<!--简介-->
@@ -202,16 +200,16 @@
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<BitmapIcon
<shci:CachedImage
Grid.Column="0"
Width="27.2"
Height="27.2"
UriSource="{Binding Selected.FetterInfo.VisionBefore,Converter={StaticResource ElementNameIconConverter}}"/>
<BitmapIcon
Source="{Binding Selected.FetterInfo.VisionBefore,Converter={StaticResource ElementNameIconConverter}}"/>
<shci:CachedImage
Grid.Column="1"
Width="27.2"
Height="27.2"
UriSource="{Binding Selected.Weapon,Converter={StaticResource WeaponTypeIconConverter}}"/>
Source="{Binding Selected.Weapon,Converter={StaticResource WeaponTypeIconConverter}}"/>
</Grid>
<shvc:ItemIcon
Height="100"

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity;
using System.Net.Http;

View File

@@ -42,6 +42,12 @@ public struct PlayerUid
}
}
/// <inheritdoc/>
public override string ToString()
{
return Value;
}
private static string EvaluateRegion(char first)
{
return first switch

View File

@@ -3,7 +3,7 @@
using System.Net.Http.Headers;
namespace Snap.Hutao.Extension;
namespace Snap.Hutao.Web;
/// <summary>
/// <see cref="HttpRequestHeaders"/> 扩展

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
@@ -22,7 +23,7 @@ namespace Snap.Hutao.Web.Hutao;
[Injection(InjectAs.Transient)]
internal class HutaoClient : ISupportAsyncInitialization
{
private const string AuthAPIHost = "https://auth.snapgenshin.com";
private const string AuthHost = "https://auth.snapgenshin.com";
private const string HutaoAPI = "https://hutao-api.snapgenshin.com";
private readonly HttpClient httpClient;
@@ -56,11 +57,11 @@ internal class HutaoClient : ISupportAsyncInitialization
if (!IsInitialized)
{
Auth auth = new(
"08d9e212-0cb3-4d71-8ed7-003606da7b20",
"7ueWgZGn53dDhrm8L5ZRw+YWfOeSWtgQmJWquRgaygw=");
"08da6c59-da3b-48dd-8cf3-e3935a7f1d4f",
"ox5dwglSXYgenK2YBc8KrAVPoQbIJ4eHfUciE+05WfI=");
HttpResponseMessage response = await httpClient
.PostAsJsonAsync($"{AuthAPIHost}/Auth/Login", auth, jsonSerializerOptions, token)
.PostAsJsonAsync($"{AuthHost}/Auth/Login", auth, jsonSerializerOptions, token)
.ConfigureAwait(false);
Response<Token>? resp = await response.Content
.ReadFromJsonAsync<Response<Token>>(jsonSerializerOptions, token)
@@ -74,12 +75,13 @@ internal class HutaoClient : ISupportAsyncInitialization
}
/// <summary>
/// 检查对应的uid当前是否上传了数据
/// 异步检查对应的uid当前是否上传了数据
/// GET /Record/CheckRecord/{Uid}
/// </summary>
/// <param name="uid">uid</param>
/// <param name="token">取消令牌</param>
/// <returns>当前是否上传了数据</returns>
public async Task<bool> CheckPeriodRecordUploadedAsync(string uid, CancellationToken token = default)
public async Task<bool> CheckPeriodRecordUploadedAsync(PlayerUid uid, CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
@@ -90,8 +92,27 @@ internal class HutaoClient : ISupportAsyncInitialization
return resp is { Data: not null, Data.PeriodUploaded: true };
}
/// <summary>
/// 异步获取排行信息
/// GET /Record/Rank/{Uid}
/// </summary>
/// <param name="uid">uid</param>
/// <param name="token">取消令牌</param>
/// <returns>排行信息</returns>
public async Task<RankInfoWrapper?> GetRankInfoAsync(PlayerUid uid, CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<RankInfoWrapper>? resp = await httpClient
.GetFromJsonAsync<Response<RankInfoWrapper>>($"{HutaoAPI}/Record/Rank/{uid}", token)
.ConfigureAwait(false);
return resp?.Data;
}
/// <summary>
/// 异步获取总览数据
/// GET /Statistics/Overview
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>总览信息</returns>
@@ -108,6 +129,7 @@ internal class HutaoClient : ISupportAsyncInitialization
/// <summary>
/// 异步获取角色出场率
/// GET /Statistics/AvatarParticipation
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色出场率</returns>
@@ -122,8 +144,26 @@ internal class HutaoClient : ISupportAsyncInitialization
return EnumerableExtensions.EmptyIfNull(resp?.Data);
}
/// <summary>
/// 异步获取角色使用率
/// GET /Statistics2/AvatarParticipation
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色出场率</returns>
public async Task<IEnumerable<AvatarParticipation>> GetAvatarParticipations2Async(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<IEnumerable<AvatarParticipation>>? resp = await httpClient
.GetFromJsonAsync<Response<IEnumerable<AvatarParticipation>>>($"{HutaoAPI}/Statistics2/AvatarParticipation", token)
.ConfigureAwait(false);
return EnumerableExtensions.EmptyIfNull(resp?.Data);
}
/// <summary>
/// 异步获取角色圣遗物搭配
/// GET /Statistics/AvatarReliquaryUsage
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色圣遗物搭配</returns>
@@ -140,6 +180,7 @@ internal class HutaoClient : ISupportAsyncInitialization
/// <summary>
/// 异步获取角色搭配数据
/// GET /Statistics/TeamCollocation
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色搭配数据</returns>
@@ -156,6 +197,7 @@ internal class HutaoClient : ISupportAsyncInitialization
/// <summary>
/// 异步获取角色武器搭配数据
/// GET /Statistics/AvatarWEaponUsage
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色武器搭配数据</returns>
@@ -172,6 +214,7 @@ internal class HutaoClient : ISupportAsyncInitialization
/// <summary>
/// 异步获取角色命座信息
/// GET /Statistics/Constellation
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色图片列表</returns>
@@ -188,6 +231,7 @@ internal class HutaoClient : ISupportAsyncInitialization
/// <summary>
/// 异步获取队伍出场次数 层间
/// GET /Statistics/TeamCombination
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>队伍出场列表</returns>
@@ -204,10 +248,11 @@ internal class HutaoClient : ISupportAsyncInitialization
/// <summary>
/// 异步获取队伍出场次数 层
/// GET /Statistics2/TeamCombination
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>队伍出场列表</returns>
public async Task<IEnumerable<TeamCombination2>> GetTeamCombination2sAsync(CancellationToken token = default)
public async Task<IEnumerable<TeamCombination2>> GetTeamCombinations2Async(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
@@ -219,7 +264,8 @@ internal class HutaoClient : ISupportAsyncInitialization
}
/// <summary>
/// 按角色列表异步获取推荐队伍
/// 异步按角色列表异步获取推荐队伍
/// POST /Statistics2/TeamRecommanded
/// </summary>
/// <param name="floor">楼层</param>
/// <param name="avatarIds">期望的角色,按期望出现顺序排序</param>
@@ -242,48 +288,6 @@ internal class HutaoClient : ISupportAsyncInitialization
return EnumerableExtensions.EmptyIfNull(resp?.Data);
}
/// <summary>
/// 异步获取角色图片列表
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色图片列表</returns>
public async Task<IEnumerable<Item>> GetAvatarMapAsync(CancellationToken token = default)
{
Response<IEnumerable<Item>>? resp = await httpClient
.GetFromJsonAsync<Response<IEnumerable<Item>>>($"{HutaoAPI}/GenshinItem/Avatars", token)
.ConfigureAwait(false);
return EnumerableExtensions.EmptyIfNull(resp?.Data);
}
/// <summary>
/// 异步获取武器图片列表
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>武器图片列表</returns>
public async Task<IEnumerable<Item>> GetWeaponMapAsync(CancellationToken token = default)
{
Response<IEnumerable<Item>>? resp = await httpClient
.GetFromJsonAsync<Response<IEnumerable<Item>>>($"{HutaoAPI}/GenshinItem/Weapons", token)
.ConfigureAwait(false);
return EnumerableExtensions.EmptyIfNull(resp?.Data);
}
/// <summary>
/// 异步获取圣遗物图片列表
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>圣遗物图片列表</returns>
public async Task<IEnumerable<Item>> GetReliquaryMapAsync(CancellationToken token = default)
{
Response<IEnumerable<Item>>? resp = await httpClient
.GetFromJsonAsync<Response<IEnumerable<Item>>>($"{HutaoAPI}/GenshinItem/Reliquaries", token)
.ConfigureAwait(false);
return EnumerableExtensions.EmptyIfNull(resp?.Data);
}
/// <summary>
/// 异步获取角色的深渊记录
/// </summary>
@@ -311,6 +315,7 @@ internal class HutaoClient : ISupportAsyncInitialization
/// <summary>
/// 异步上传记录
/// POST /Record/Upload
/// </summary>
/// <param name="playerRecord">玩家记录</param>
/// <param name="token">取消令牌</param>
@@ -320,8 +325,13 @@ internal class HutaoClient : ISupportAsyncInitialization
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{HutaoAPI}/Record/Upload", playerRecord, jsonSerializerOptions, token);
return await response.Content.ReadFromJsonAsync<Response<string>>(jsonSerializerOptions, token);
HttpResponseMessage response = await httpClient
.PostAsJsonAsync($"{HutaoAPI}/Record/Upload", playerRecord, jsonSerializerOptions, token)
.ConfigureAwait(false);
return await response.Content
.ReadFromJsonAsync<Response<string>>(jsonSerializerOptions, token)
.ConfigureAwait(false);
}
private class Auth

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hutao.Model.Post;
/// <summary>
/// 伤害信息
/// </summary>
public class Damage
{
/// <summary>
/// 构造一个新的伤害信息
/// </summary>
/// <param name="avatarId">角色Id</param>
/// <param name="value">值</param>
public Damage(int avatarId, int value)
{
AvatarId = avatarId;
Value = value;
}
/// <summary>
/// 角色Id
/// </summary>
public int AvatarId { get; set; }
/// <summary>
/// 值
/// </summary>
public int Value { get; set; }
}

View File

@@ -16,32 +16,36 @@ namespace Snap.Hutao.Web.Hutao.Model.Post;
public class PlayerRecord
{
/// <summary>
/// 构造一个新的玩家记录
/// 防止从外部构造一个新的玩家记录
/// </summary>
/// <param name="uid">uid</param>
/// <param name="playerAvatars">玩家角色</param>
/// <param name="playerSpiralAbyssesLevels">玩家深渊信息</param>
private PlayerRecord(string uid, IEnumerable<PlayerAvatar> playerAvatars, IEnumerable<PlayerSpiralAbyssLevel> playerSpiralAbyssesLevels)
private PlayerRecord()
{
Uid = uid;
PlayerAvatars = playerAvatars;
PlayerSpiralAbyssesLevels = playerSpiralAbyssesLevels;
}
/// <summary>
/// uid
/// </summary>
public string Uid { get; }
public string Uid { get; private set; } = default!;
/// <summary>
/// 玩家角色
/// </summary>
public IEnumerable<PlayerAvatar> PlayerAvatars { get; }
public IEnumerable<PlayerAvatar> PlayerAvatars { get; private set; } = default!;
/// <summary>
/// 玩家深渊信息
/// </summary>
public IEnumerable<PlayerSpiralAbyssLevel> PlayerSpiralAbyssesLevels { get; }
public IEnumerable<PlayerSpiralAbyssLevel> PlayerSpiralAbyssesLevels { get; private set; } = default!;
/// <summary>
/// 造成最多伤害
/// </summary>
public Damage? DamageMost { get; private set; }
/// <summary>
/// 承受最多伤害
/// </summary>
public Damage? TakeDamageMost { get; private set; }
/// <summary>
/// 建造玩家记录
@@ -59,7 +63,14 @@ public class PlayerRecord
.SelectMany(f => f.Levels, (f, level) => new IndexedLevel(f.Index, level))
.Select(indexedLevel => new PlayerSpiralAbyssLevel(indexedLevel));
return new PlayerRecord(uid, playerAvatars, playerSpiralAbyssLevels);
return new()
{
Uid = uid,
PlayerAvatars = playerAvatars,
PlayerSpiralAbyssesLevels = playerSpiralAbyssLevels,
DamageMost = GetDamage(spiralAbyss.DamageRank),
TakeDamageMost = GetDamage(spiralAbyss.TakeDamageRank),
};
}
/// <summary>
@@ -68,8 +79,19 @@ public class PlayerRecord
/// <param name="hutaoClient">使用的客户端</param>
/// <param name="token">取消令牌</param>
/// <returns>上传结果</returns>
internal Task<Response<string>?> UploadRecordAsync(HutaoClient hutaoClient, CancellationToken token = default)
internal Task<Response<string>?> UploadAsync(HutaoClient hutaoClient, CancellationToken token = default)
{
return hutaoClient.UploadRecordAsync(this, token);
}
private static Damage? GetDamage(List<RankInfo> ranks)
{
if (ranks.Count > 0)
{
RankInfo rank = ranks[0];
return new Damage(rank.AvatarId, rank.Value);
}
return null;
}
}

View File

@@ -0,0 +1,34 @@
// 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;
/// <summary>
/// 排行信息
/// </summary>
public class RankInfo
{
/// <summary>
/// 角色Id
/// </summary>
public int AvatarId { get; set; }
/// <summary>
/// 值
/// </summary>
public int Value { get; set; }
/// <summary>
/// 百分比
/// </summary>
public double Percent { get; set; }
/// <summary>
/// 总体百分比
/// </summary>
public double PercentTotal { get; set; }
}

View File

@@ -0,0 +1,24 @@
// 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;
/// <summary>
/// 排行包装
/// </summary>
public class RankInfoWrapper
{
/// <summary>
/// 伤害
/// </summary>
public RankInfo? Damage { get; set; }
/// <summary>
/// 承受伤害
/// </summary>
public RankInfo? TakeDamage { get; set; }
}

View File

@@ -21,4 +21,4 @@ public class ReliquarySets : List<ReliquarySet>
: base(sets)
{
}
}
}