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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.2" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

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

View File

@@ -13,8 +13,6 @@
<!--Modify Window title bar color--> <!--Modify Window title bar color-->
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush" /> <StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush" />
<StaticResource x:Key="WindowCaptionBackgroundDisabled" 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" /> <StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush" />

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ public class SystemBackdrop
{ {
private readonly Window window; private readonly Window window;
private WindowsSystemDispatcherQueueHelper? dispatcherQueueHelper; private DispatcherQueueHelper? dispatcherQueueHelper;
private MicaController? backdropController; private MicaController? backdropController;
private SystemBackdropConfiguration? configurationSource; private SystemBackdropConfiguration? configurationSource;
@@ -25,7 +25,6 @@ public class SystemBackdrop
/// 构造一个新的系统背景帮助类 /// 构造一个新的系统背景帮助类
/// </summary> /// </summary>
/// <param name="window">窗体</param> /// <param name="window">窗体</param>
/// <param name="fallBackBehavior">回退行为</param>
public SystemBackdrop(Window window) public SystemBackdrop(Window window)
{ {
this.window = window; this.window = window;
@@ -49,7 +48,7 @@ public class SystemBackdrop
} }
else else
{ {
dispatcherQueueHelper = new WindowsSystemDispatcherQueueHelper(); dispatcherQueueHelper = new DispatcherQueueHelper();
dispatcherQueueHelper.EnsureWindowsSystemDispatcherQueueController(); dispatcherQueueHelper.EnsureWindowsSystemDispatcherQueueController();
// Hooking up the policy object // Hooking up the policy object
@@ -104,16 +103,16 @@ public class SystemBackdrop
{ {
Must.NotNull(configurationSource!).Theme = ((FrameworkElement)window.Content).ActualTheme switch Must.NotNull(configurationSource!).Theme = ((FrameworkElement)window.Content).ActualTheme switch
{ {
ElementTheme.Dark => SystemBackdropTheme.Dark,
ElementTheme.Light => SystemBackdropTheme.Light,
ElementTheme.Default => SystemBackdropTheme.Default, ElementTheme.Default => SystemBackdropTheme.Default,
ElementTheme.Light => SystemBackdropTheme.Light,
ElementTheme.Dark => SystemBackdropTheme.Dark,
_ => throw Must.NeverHappen(), _ => throw Must.NeverHappen(),
}; };
} }
private class WindowsSystemDispatcherQueueHelper private class DispatcherQueueHelper
{ {
private object dispatcherQueueController = null!; private object? dispatcherQueueController = null;
/// <summary> /// <summary>
/// 确保系统调度队列控制器存在 /// 确保系统调度队列控制器存在
@@ -128,19 +127,21 @@ public class SystemBackdrop
if (dispatcherQueueController == null) if (dispatcherQueueController == null)
{ {
DispatcherQueueOptions options; DispatcherQueueOptions options = new()
options.DwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions)); {
options.ThreadType = 2; // DQTYPE_THREAD_CURRENT DwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions)),
options.ApartmentType = 2; // DQTAT_COM_STA ThreadType = 2, // DQTYPE_THREAD_CURRENT
ApartmentType = 2, // DQTAT_COM_STA
};
_ = CreateDispatcherQueueController(options, ref dispatcherQueueController!); _ = CreateDispatcherQueueController(options, ref dispatcherQueueController);
} }
} }
[DllImport("CoreMessaging.dll")] [DllImport("CoreMessaging.dll")]
private static extern int CreateDispatcherQueueController( private static extern int CreateDispatcherQueueController(
[In] DispatcherQueueOptions options, [In] DispatcherQueueOptions options,
[In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object dispatcherQueueController); [In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object? dispatcherQueueController);
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
private struct DispatcherQueueOptions 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. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.WinUI.UI;
using CommunityToolkit.WinUI.UI.Controls; using CommunityToolkit.WinUI.UI.Controls;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Extension; using Snap.Hutao.Extension;
namespace Snap.Hutao.Control.Image; namespace Snap.Hutao.Control.Image;
@@ -14,11 +14,15 @@ namespace Snap.Hutao.Control.Image;
/// </summary> /// </summary>
public class CachedImage : ImageEx public class CachedImage : ImageEx
{ {
private readonly IImageCache imageCache;
/// <summary> /// <summary>
/// 构造一个新的缓存图像 /// 构造一个新的缓存图像
/// </summary> /// </summary>
public CachedImage() public CachedImage()
{ {
imageCache = Ioc.Default.GetRequiredService<IImageCache>();
IsCacheEnabled = true; IsCacheEnabled = true;
EnableLazyLoading = true; EnableLazyLoading = true;
} }
@@ -29,7 +33,7 @@ public class CachedImage : ImageEx
BitmapImage? image; BitmapImage? image;
try try
{ {
image = await ImageCache.Instance.GetFromCacheAsync(imageUri, true, token); image = await imageCache.GetFromCacheAsync(imageUri, true, token);
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
@@ -39,7 +43,7 @@ public class CachedImage : ImageEx
catch catch
{ {
// maybe the image is corrupted, remove it. // maybe the image is corrupted, remove it.
await ImageCache.Instance.RemoveAsync(imageUri.Enumerate()); await imageCache.RemoveAsync(imageUri.Enumerate());
throw; throw;
} }

View File

@@ -8,11 +8,11 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting; using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Snap.Hutao.Context.FileSystem; using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.Threading; using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension; using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using System.Numerics; using System.Numerics;
using Windows.Graphics.Imaging; using Windows.Graphics.Imaging;
using Windows.Storage; using Windows.Storage;
@@ -26,7 +26,8 @@ namespace Snap.Hutao.Control.Image;
public class Gradient : Microsoft.UI.Xaml.Controls.Control public class Gradient : Microsoft.UI.Xaml.Controls.Control
{ {
private static readonly DependencyProperty SourceProperty = Property<Gradient>.Depend(nameof(Source), string.Empty, OnSourceChanged); 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 SpriteVisual? spriteVisual;
private double imageAspectRatio; private double imageAspectRatio;
@@ -47,31 +48,20 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control
set => SetValue(SourceProperty, value); 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) private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
{ {
Gradient gradient = (Gradient)sender; Gradient gradient = (Gradient)sender;
string url = (string)arg.NewValue; 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) private void OnSizeChanged(object sender, SizeChangedEventArgs e)
@@ -98,7 +88,7 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control
{ {
await AnimationBuilder await AnimationBuilder
.Create() .Create()
.Opacity(0, 1) .Opacity(0d)
.StartAsync(this, token); .StartAsync(this, token);
StorageFile storageFile = await GetCachedFileAsync(url, 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 gradientEffectBrush = compositor.CompositeBlendEffectBrush(backgroundBrush, foregroundBrush);
CompositionEffectBrush opacityMaskEffectBrush = compositor.CompositeLuminanceToAlphaEffectBrush(gradientEffectBrush); CompositionEffectBrush opacityMaskEffectBrush = compositor.CompositeLuminanceToAlphaEffectBrush(gradientEffectBrush);
compositor.CreateMaskBrush();
CompositionEffectBrush alphaMaskEffectBrush = compositor.CompositeAlphaMaskEffectBrush(imageSurfaceBrush, opacityMaskEffectBrush); CompositionEffectBrush alphaMaskEffectBrush = compositor.CompositeAlphaMaskEffectBrush(imageSurfaceBrush, opacityMaskEffectBrush);
spriteVisual = compositor.CompositeSpriteVisual(alphaMaskEffectBrush); spriteVisual = compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
@@ -123,10 +114,40 @@ public class Gradient : Microsoft.UI.Xaml.Controls.Control
await AnimationBuilder await AnimationBuilder
.Create() .Create()
.Opacity(1, 0) .Opacity(1d)
.StartAsync(this, token); .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) private async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
{ {
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(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); return LoadedImageSurface.StartLoadFromStream(imageStream);
} }
} }
} }

View File

@@ -24,21 +24,4 @@ public static class Browser
failAction?.Invoke(ex); 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>
/// 核心环境参数 /// 核心环境参数
/// </summary> /// </summary>
internal static class CoreEnvironment internal static partial class CoreEnvironment
{ {
/// <summary> /// <summary>
/// 当前版本 /// 当前版本
@@ -43,3 +43,8 @@ internal static class CoreEnvironment
return Md5Convert.ToHexString($"{userName}{machineGuid}"); 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> /// </summary>
internal static class EventIds 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>
/// 导航失败 /// 导航失败
/// </summary> /// </summary>
public static readonly EventId NavigationFailed = new(100000, nameof(NavigationFailed)); public static readonly EventId NavigationFailed = 100101;
/// <summary> /// <summary>
/// 未处理的异常 /// 元数据初始化过程
/// </summary> /// </summary>
public static readonly EventId UnhandledException = new(100001, nameof(UnhandledException)); public static readonly EventId MetadataInitialization = 100110;
/// <summary> /// <summary>
/// Forget任务执行异常 /// 元数据文件MD5检查
/// </summary> /// </summary>
public static readonly EventId TaskException = new(100002, nameof(TaskException)); public static readonly EventId MetadataFileMD5Check = 100111;
// 杂项
/// <summary> /// <summary>
/// Forget任务执行异常 /// 杂项Log
/// </summary> /// </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. // Licensed under the MIT license.
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core.Logging;
namespace Snap.Hutao.Core; namespace Snap.Hutao.Core;
@@ -36,7 +37,8 @@ internal abstract class WebView2Helper
} }
catch (Exception ex) 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; isSupported = false;
} }

View File

@@ -60,7 +60,9 @@ internal static class CompositionExtensions
/// <param name="compositor">合成器</param> /// <param name="compositor">合成器</param>
/// <param name="sourceBrush">源</param> /// <param name="sourceBrush">源</param>
/// <returns>合成效果画刷</returns> /// <returns>合成效果画刷</returns>
public static CompositionEffectBrush CompositeLuminanceToAlphaEffectBrush(this Compositor compositor, CompositionBrush sourceBrush) public static CompositionEffectBrush CompositeLuminanceToAlphaEffectBrush(
this Compositor compositor,
CompositionBrush sourceBrush)
{ {
LuminanceToAlphaEffect effect = new() LuminanceToAlphaEffect effect = new()
{ {
@@ -150,5 +152,24 @@ internal static class CompositionExtensions
return brush; 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); public record struct GradientStop(float Offset, Windows.UI.Color Color);
} }

View File

@@ -9,19 +9,8 @@ namespace Snap.Hutao.Extension;
/// <summary> /// <summary>
/// <see cref="IEnumerable{T}"/> 扩展 /// <see cref="IEnumerable{T}"/> 扩展
/// </summary> /// </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>
/// 计数 /// 计数
/// </summary> /// </summary>
@@ -66,6 +55,17 @@ public static class EnumerableExtensions
return source ?? new(); 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> /// <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); 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>
/// 任务扩展 /// 任务扩展
/// </summary> /// </summary>
[SuppressMessage("", "VSTHRD003")]
[SuppressMessage("", "VSTHRD100")]
public static class TaskExtensions public static class TaskExtensions
{ {
/// <summary> /// <summary>
/// 安全的触发任务 /// 安全的触发任务
/// </summary> /// </summary>
/// <param name="task">任务</param> /// <param name="task">任务</param>
/// <param name="continueOnCapturedContext">是否在捕获的上下文中继续执行</param> public static async void SafeForget(this Task task)
/// <param name="logger">日志器</param>
[SuppressMessage("", "VSTHRD003")]
[SuppressMessage("", "VSTHRD100")]
public static async void SafeForget(this Task task, bool continueOnCapturedContext = true, ILogger? logger = null)
{ {
try 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) catch (TaskCanceledException)
{ {
// Do nothing
} }
catch (Exception e) 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 } }; 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) if (asyncRelayCommand.ExecutionTask?.Exception is AggregateException exception)
{ {
Exception baseException = exception.GetBaseException(); 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()) if (context.Database.GetPendingMigrations().Any())
{ {
Debug.WriteLine("Performing Migrations"); Debug.WriteLine("Performing AppDbContext Migrations");
context.Database.Migrate(); context.Database.Migrate();
} }
} }

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Service.Metadata; using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Enka; using Snap.Hutao.Web.Enka;
using Snap.Hutao.Web.Hoyolab.Bbs.User; using Snap.Hutao.Web.Hoyolab.Bbs.User;
@@ -29,6 +30,7 @@ internal static class IocHttpClientConfiguration
{ {
// services // services
services.AddHttpClient<MetadataService>(DefaultConfiguration); services.AddHttpClient<MetadataService>(DefaultConfiguration);
// services.AddHttpClient<ImageCache>(DefaultConfiguration);
// normal clients // normal clients
services.AddHttpClient<AnnouncementClient>(DefaultConfiguration); services.AddHttpClient<AnnouncementClient>(DefaultConfiguration);

View File

@@ -4,9 +4,9 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.Context.Database; using Snap.Hutao.Context.Database;
using Snap.Hutao.Control.HostBackdrop; using Snap.Hutao.Control.HostBackdrop;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Win32; using Snap.Hutao.Core.Win32;
using System.Drawing;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using WinRT.Interop; using WinRT.Interop;
@@ -47,32 +47,7 @@ public sealed partial class MainWindow : Window
return new RECT(left, top, right, bottom); return new RECT(left, top, right, bottom);
} }
private void InitializeWindow() private static void SaveWindowRect(IntPtr handle)
{
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()
{ {
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Default; WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Default;
User32.GetWindowPlacement(handle, ref windowPlacement); User32.GetWindowPlacement(handle, ref windowPlacement);
@@ -83,12 +58,36 @@ public sealed partial class MainWindow : Window
LocalSetting.SetValueType(SettingKeys.WindowBottom, windowPlacement.NormalPosition.Bottom); 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) private void MainWindowClosed(object sender, WindowEventArgs args)
{ {
SaveWindowRect(); SaveWindowRect(handle);
// save datebase // save datebase
int changes = appDbContext.SaveChanges(); 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 namespace Snap.Hutao.Migrations
{ {
[DbContext(typeof(AppDbContext))] [DbContext(typeof(AppDbContext))]
[Migration("20220618110357_SettingAndUser")] [Migration("20220720121642_Init")]
partial class SettingAndUser partial class Init
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #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 => modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{ {

View File

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

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #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 => 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; Avatar.Avatar avatar = (Avatar.Avatar)value;
string avatarName = avatar.Icon[14..]; string avatarName = ReplaceSpecialCaseNaming(avatar.Icon[14..]);
return new Uri(string.Format(BaseUrl, avatarName)); return new Uri(string.Format(BaseUrl, avatarName));
} }
private static string ReplaceSpecialCaseNaming(string avatarName)
{
return avatarName switch
{
"Yae" => "Yae1",
_ => avatarName,
};
}
/// <inheritdoc/> /// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language) public object ConvertBack(object value, Type targetType, object parameter, string language)
{ {

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement; using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using System.Collections.Generic; using System.Collections.Generic;
@@ -15,7 +14,7 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Transient, typeof(IAnnouncementService))] [Injection(InjectAs.Transient, typeof(IAnnouncementService))]
internal class AnnouncementService : 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 AnnouncementClient announcementClient;
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using Microsoft.VisualStudio.Threading; using Microsoft.VisualStudio.Threading;
using Snap.Hutao.Core; using Snap.Hutao.Core;
@@ -14,6 +15,18 @@ namespace Snap.Hutao.View.Page;
/// </summary> /// </summary>
public sealed partial class AnnouncementContentPage : Microsoft.UI.Xaml.Controls.Page 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. // support click open browser.
private const string MihoyoSDKDefinition = private const string MihoyoSDKDefinition =
@"window.miHoYoGameJSSDK = { @"window.miHoYoGameJSSDK = {
@@ -49,7 +62,7 @@ openInWebview: function(url){ location.href = url }}";
await WebView.EnsureCoreWebView2Async(); await WebView.EnsureCoreWebView2Async();
await WebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(MihoyoSDKDefinition); await WebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(MihoyoSDKDefinition);
WebView.CoreWebView2.WebMessageReceived += (_, e) => Browser.Open(e.TryGetWebMessageAsString); WebView.CoreWebView2.WebMessageReceived += (_, e) => Browser.Open(e.TryGetWebMessageAsString());
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -57,7 +70,39 @@ openInWebview: function(url){ location.href = url }}";
return; return;
} }
WebView.NavigateToString(targetContent); WebView.NavigateToString(ReplaceForeground(targetContent, ActualTheme));
data.NotifyNavigationCompleted(); 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:shca="using:Snap.Hutao.Control.Animation"
xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shcc="using:Snap.Hutao.Control.Cancellable" xmlns:shcc="using:Snap.Hutao.Control.Cancellable"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shvc="using:Snap.Hutao.View.Converter" xmlns:shvc="using:Snap.Hutao.View.Converter"
mc:Ignorable="d" mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors> <mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Loaded"> <shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<mxic:InvokeCommandAction Command="{Binding OpenUICommand}"/>
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors> </mxi:Interaction.Behaviors>
<shcc:CancellablePage.Resources> <shcc:CancellablePage.Resources>
<cwuconv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/> <cwuconv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
@@ -75,11 +74,14 @@
<mxi:Interaction.Behaviors> <mxi:Interaction.Behaviors>
<shcb:AutoHeightBehavior TargetWidth="1080" TargetHeight="390"/> <shcb:AutoHeightBehavior TargetWidth="1080" TargetHeight="390"/>
</mxi:Interaction.Behaviors> </mxi:Interaction.Behaviors>
<Border.Background> <shci:CachedImage
Stretch="UniformToFill"
Source="{Binding Banner}"/>
<!--<Border.Background>
<ImageBrush <ImageBrush
ImageSource="{Binding Banner}" ImageSource="{Binding Banner}"
Stretch="UniformToFill"/> Stretch="UniformToFill"/>
</Border.Background> </Border.Background>-->
<cwua:Explicit.Animations> <cwua:Explicit.Animations>
<cwua:AnimationSet x:Name="ImageZoomInAnimation"> <cwua:AnimationSet x:Name="ImageZoomInAnimation">
<shca:ImageZoomInAnimation/> <shca:ImageZoomInAnimation/>

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using System.Net.Http; 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) private static string EvaluateRegion(char first)
{ {
return first switch return first switch

View File

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

View File

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