diff --git a/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/CacheContext.cs b/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/CacheContext.cs new file mode 100644 index 00000000..6a5c1b89 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/CacheContext.cs @@ -0,0 +1,64 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Context.FileSystem.Location; +using Windows.Storage; + +namespace Snap.Hutao.Context.FileSystem; + +/// +/// 缓存目录上下文 +/// +[Injection(InjectAs.Transient)] +internal class CacheContext : FileSystemContext +{ + /// + /// 构造一个新的缓存目录上下文 + /// + /// 缓存位置 + public CacheContext(Cache cache) + : base(cache) + { + } + + /// + /// 获取缓存文件夹 + /// + public static StorageFolder Folder + { + get => ApplicationData.Current.TemporaryFolder; + } + + /// + /// 获取缓存文件的名称 + /// + /// uri + /// 缓存文件的名称 + public static string GetCacheFileName(Uri uri) + { + return CreateHash64(uri.ToString()).ToString(); + } + + /// + /// 获取缓存文件的名称 + /// + /// url + /// 缓存文件的名称 + public static string GetCacheFileName(string url) + { + return CreateHash64(url).ToString(); + } + + private static ulong CreateHash64(string str) + { + byte[] utf8 = 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; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/Cache.cs b/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/Cache.cs new file mode 100644 index 00000000..315cdbfa --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/Cache.cs @@ -0,0 +1,26 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.IO; + +namespace Snap.Hutao.Context.FileSystem.Location; + +/// +/// 缓存位置 +/// +[Injection(InjectAs.Transient)] +internal class Cache : IFileSystemLocation +{ + private string? path; + + /// + public string GetPath() + { + if (string.IsNullOrEmpty(path)) + { + path = Windows.Storage.ApplicationData.Current.TemporaryFolder.Path; + } + + return path; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/Metadata.cs b/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/Metadata.cs index 7110d6a8..5f967ce4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/Metadata.cs +++ b/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/Metadata.cs @@ -9,7 +9,7 @@ namespace Snap.Hutao.Context.FileSystem.Location; /// 我的文档位置 /// [Injection(InjectAs.Transient)] -public class Metadata : IFileSystemLocation +internal class Metadata : IFileSystemLocation { private string? path; diff --git a/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/MyDocument.cs b/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/MyDocument.cs index 1dfa1a24..5bb9e34f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/MyDocument.cs +++ b/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/Location/MyDocument.cs @@ -9,7 +9,7 @@ namespace Snap.Hutao.Context.FileSystem.Location; /// 我的文档位置 /// [Injection(InjectAs.Transient)] -public class MyDocument : IFileSystemLocation +internal class MyDocument : IFileSystemLocation { private string? path; diff --git a/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/MetadataContext.cs b/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/MetadataContext.cs index f50a29ca..da417975 100644 --- a/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/MetadataContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Context/FileSystem/MetadataContext.cs @@ -16,4 +16,4 @@ internal class MetadataContext : FileSystemContext : base(metadata) { } -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/Gradient.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/Gradient.cs new file mode 100644 index 00000000..2f3d9c31 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/Gradient.cs @@ -0,0 +1,140 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.WinUI.Helpers; +using CommunityToolkit.WinUI.UI.Animations; +using Microsoft.UI; +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Hosting; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Animation; +using Snap.Hutao.Context.FileSystem; +using Snap.Hutao.Core; +using Snap.Hutao.Core.Threading; +using Snap.Hutao.Extension; +using System.Numerics; +using Windows.Graphics.Imaging; +using Windows.Storage; +using Windows.Storage.Streams; + +namespace Snap.Hutao.Control.Image; + +/// +/// 支持渐变的图像 +/// +public class Gradient : Microsoft.UI.Xaml.Controls.Control +{ + private static readonly DependencyProperty SourceProperty = Property.Depend(nameof(Source), string.Empty, OnSourceChanged); + private static readonly ConcurrentCancellationTokenSource ImageLoading = new(); + private SpriteVisual? spriteVisual; + private double imageAspectRatio; + + /// + /// 构造一个新的渐变图像 + /// + public Gradient() + { + SizeChanged += OnSizeChanged; + } + + /// + /// 源 + /// + public string Source + { + get => (string)GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + private static async Task GetCachedFileAsync(string url, CancellationToken token) + { + string fileName = CacheContext.GetCacheFileName(url); + CacheContext cacheContext = Ioc.Default.GetRequiredService(); + + StorageFile storageFile; + if (!cacheContext.FileExists(fileName)) + { + storageFile = await CacheContext.Folder.CreateFileAsync(fileName).AsTask(token); + await StreamHelper.GetHttpStreamToStorageFileAsync(new(url), storageFile); + } + else + { + storageFile = await CacheContext.Folder.GetFileAsync(fileName).AsTask(token); + } + + return storageFile; + } + + private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg) + { + Gradient gradient = (Gradient)sender; + string url = (string)arg.NewValue; + + gradient.ApplyImageAsync(url, ImageLoading.Register(gradient)).SafeForget(); + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + if (e.NewSize != e.PreviousSize && spriteVisual is not null) + { + UpdateVisual(spriteVisual); + } + } + + private void UpdateVisual(SpriteVisual spriteVisual) + { + if (spriteVisual is not null) + { + double width = ActualWidth; + double height = Math.Clamp(width * imageAspectRatio, 0, MaxHeight); + + spriteVisual.Size = new Vector2((float)width, (float)height); + Height = height; + } + } + + private async Task ApplyImageAsync(string url, CancellationToken token) + { + await AnimationBuilder + .Create() + .Opacity(0, 1) + .StartAsync(this, token); + + StorageFile storageFile = await GetCachedFileAsync(url, token); + + Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; + + LoadedImageSurface imageSurface = await LoadImageSurfaceAsync(storageFile, token); + + CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.UniformToFill, vRatio: 0f); + + CompositionLinearGradientBrush backgroundBrush = compositor.CompositeLinearGradientBrush(new(1f, 0), Vector2.UnitY, new(0, Colors.White), new(1, Colors.Black)); + CompositionLinearGradientBrush foregroundBrush = compositor.CompositeLinearGradientBrush(Vector2.Zero, Vector2.UnitY, new(0, Colors.White), new(0.95f, Colors.Black)); + + CompositionEffectBrush gradientEffectBrush = compositor.CompositeBlendEffectBrush(backgroundBrush, foregroundBrush); + CompositionEffectBrush opacityMaskEffectBrush = compositor.CompositeLuminanceToAlphaEffectBrush(gradientEffectBrush); + CompositionEffectBrush alphaMaskEffectBrush = compositor.CompositeAlphaMaskEffectBrush(imageSurfaceBrush, opacityMaskEffectBrush); + + spriteVisual = compositor.CompositeSpriteVisual(alphaMaskEffectBrush); + UpdateVisual(spriteVisual); + + ElementCompositionPreview.SetElementChildVisual(this, spriteVisual); + + await AnimationBuilder + .Create() + .Opacity(1, 0) + .StartAsync(this, token); + } + + private async Task LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token) + { + using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token)) + { + BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token); + imageAspectRatio = (double)decoder.PixelHeight / decoder.PixelWidth; + + return LoadedImageSurface.StartLoadFromStream(imageStream); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs index 2a1d39c6..dfb6412e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs @@ -22,4 +22,9 @@ internal static class EventIds /// Forget任务执行异常 /// public static readonly EventId TaskException = new(100002, nameof(TaskException)); + + /// + /// Forget任务执行异常 + /// + public static readonly EventId AsyncCommandException = new(100003, nameof(AsyncCommandException)); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs new file mode 100644 index 00000000..a0cd7879 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ConcurrentCancellationTokenSource.cs @@ -0,0 +1,33 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Concurrent; + +namespace Snap.Hutao.Core.Threading; + +/// +/// 并发 +/// +/// 项类型 +internal class ConcurrentCancellationTokenSource + where TItem : notnull +{ + private readonly ConcurrentDictionary waitingItems = new(); + + /// + /// 未某个项注册取消令牌 + /// + /// 项 + /// 取消令牌 + public CancellationToken Register(TItem item) + { + if (waitingItems.TryRemove(item, out CancellationTokenSource? prevSource)) + { + prevSource.Cancel(); + } + + CancellationTokenSource current = waitingItems.GetOrAdd(item, new CancellationTokenSource()); + + return current.Token; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/CompositionExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/CompositionExtensions.cs new file mode 100644 index 00000000..9d9c8c22 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Extension/CompositionExtensions.cs @@ -0,0 +1,154 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.Graphics.Canvas.Effects; +using Microsoft.UI.Composition; +using System.Numerics; + +namespace Snap.Hutao.Extension; + +/// +/// 合成扩展 +/// +internal static class CompositionExtensions +{ + /// + /// 创建拼合图视觉对象 + /// + /// 合成器 + /// 画刷 + /// 拼合图视觉对象 + public static SpriteVisual CompositeSpriteVisual(this Compositor compositor, CompositionBrush brush) + { + SpriteVisual spriteVisual = compositor.CreateSpriteVisual(); + spriteVisual.Brush = brush; + return spriteVisual; + } + + /// + /// 创建混合效果画刷 + /// + /// 合成器 + /// 前景 + /// 背景 + /// 混合模式 + /// 合成效果画刷 + public static CompositionEffectBrush CompositeBlendEffectBrush( + this Compositor compositor, + CompositionBrush background, + CompositionBrush foreground, + BlendEffectMode blendEffectMode = BlendEffectMode.Multiply) + { + BlendEffect effect = new() + { + Background = new CompositionEffectSourceParameter("Background"), + Foreground = new CompositionEffectSourceParameter("Foreground"), + Mode = blendEffectMode, + }; + + CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); + + brush.SetSourceParameter("Background", background); + brush.SetSourceParameter("Foreground", foreground); + + return brush; + } + + /// + /// 创建亮度转不透明度效果画刷 + /// + /// 合成器 + /// 源 + /// 合成效果画刷 + public static CompositionEffectBrush CompositeLuminanceToAlphaEffectBrush(this Compositor compositor, CompositionBrush sourceBrush) + { + LuminanceToAlphaEffect effect = new() + { + Source = new CompositionEffectSourceParameter("Source"), + }; + + CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush(); + + brush.SetSourceParameter("Source", sourceBrush); + + return brush; + } + + /// + /// 创建不透明度蒙版效果画刷 + /// + /// 合成器 + /// 源 + /// 不透明度蒙版 + /// 合成效果画刷 + public static CompositionEffectBrush CompositeAlphaMaskEffectBrush( + this Compositor compositor, + CompositionBrush sourceBrush, + CompositionBrush alphaMask) + { + AlphaMaskEffect maskEffect = new() + { + AlphaMask = new CompositionEffectSourceParameter("AlphaMask"), + Source = new CompositionEffectSourceParameter("Source"), + }; + + CompositionEffectBrush brush = compositor.CreateEffectFactory(maskEffect).CreateBrush(); + + brush.SetSourceParameter("AlphaMask", alphaMask); + brush.SetSourceParameter("Source", sourceBrush); + + return brush; + } + + /// + /// 创建一个表面画刷 + /// + /// 合成器 + /// 合成表面 + /// 拉伸方法 + /// 水平对齐比 + /// 垂直对齐比 + /// 合成表面画刷 + public static CompositionSurfaceBrush CompositeSurfaceBrush( + this Compositor compositor, + ICompositionSurface surface, + CompositionStretch stretch = CompositionStretch.None, + float hRatio = 0.5f, + float vRatio = 0.5f) + { + CompositionSurfaceBrush brush = compositor.CreateSurfaceBrush(surface); + brush.Stretch = stretch; + brush.VerticalAlignmentRatio = vRatio; + brush.HorizontalAlignmentRatio = hRatio; + + return brush; + } + + /// + /// 创建一个线性渐变画刷 + /// + /// 合成器 + /// 起点 + /// 终点 + /// 锚点 + /// 线性渐变画刷 + public static CompositionLinearGradientBrush CompositeLinearGradientBrush( + this Compositor compositor, + Vector2 start, + Vector2 end, + params GradientStop[] stops) + { + CompositionLinearGradientBrush brush = compositor.CreateLinearGradientBrush(); + brush.StartPoint = start; + brush.EndPoint = end; + + foreach (GradientStop stop in stops) + { + brush.ColorStops.Add(compositor.CreateColorGradientStop(stop.Offset, stop.Color)); + } + + return brush; + } + + public record struct GradientStop(float Offset, Windows.UI.Color Color); +} diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/TaskExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/TaskExtensions.cs index 8e6f1826..8296a7af 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/TaskExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/TaskExtensions.cs @@ -24,6 +24,9 @@ public static class TaskExtensions { await task.ConfigureAwait(continueOnCapturedContext); } + catch (TaskCanceledException) + { + } catch (Exception e) { logger?.LogError(EventIds.TaskException, e, "{caller}:{exception}", nameof(SafeForget), e.GetBaseException()); diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs index 99d5393b..e9aaf1de 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/AsyncRelayCommandFactory.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.Input; -using Microsoft.AppCenter.Crashes; +using Snap.Hutao.Core.Logging; using Snap.Hutao.Factory.Abstraction; namespace Snap.Hutao.Factory; @@ -93,8 +93,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory if (asyncRelayCommand.ExecutionTask?.Exception is AggregateException exception) { Exception baseException = exception.GetBaseException(); - logger.LogError(baseException, "异步命令发生了错误"); - Crashes.TrackError(baseException); + logger.LogError(EventIds.AsyncCommandException, baseException, "异步命令发生了错误"); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs index 436cf0e9..908bbade 100644 --- a/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/IocHttpClientConfiguration.cs @@ -31,10 +31,10 @@ internal static class IocHttpClientConfiguration services.AddHttpClient(DefaultConfiguration); // normal clients - services.AddHttpClient(DefaultConfiguration); services.AddHttpClient(DefaultConfiguration); - services.AddHttpClient(DefaultConfiguration); services.AddHttpClient(DefaultConfiguration); + services.AddHttpClient(DefaultConfiguration); + services.AddHttpClient(DefaultConfiguration); // x-rpc clients services.AddHttpClient(XRpcConfiguration); diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs new file mode 100644 index 00000000..11c66cc4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AvatarNameCardPicConverter.cs @@ -0,0 +1,33 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Data; + +namespace Snap.Hutao.Model.Metadata.Converter; + +/// +/// 角色名片转换器 +/// +internal class AvatarNameCardPicConverter : IValueConverter +{ + private const string BaseUrl = "https://static.snapgenshin.com/NameCardPic/UI_NameCardPic_{0}_P.png"; + + /// + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value == null) + { + return null!; + } + + Avatar.Avatar avatar = (Avatar.Avatar)value; + string avatarName = avatar.Icon[14..]; + return new Uri(string.Format(BaseUrl, avatarName)); + } + + /// + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw Must.NeverHappen(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index 1fc6f2d8..80469ad8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -9,7 +9,7 @@ + Version="1.0.13.0" /> 胡桃 diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index a96303ce..6c1daf5b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -71,10 +71,9 @@ - - - - + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml index 5c4e6ffe..6acb29ac 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml @@ -5,7 +5,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls" - xmlns:cwum="using:CommunityToolkit.WinUI.UI.Media" xmlns:mxic="using:Microsoft.Xaml.Interactions.Core" xmlns:mxi="using:Microsoft.Xaml.Interactivity" xmlns:shc="using:Snap.Hutao.Control" @@ -27,6 +26,7 @@ + @@ -144,44 +144,48 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -391,7 +395,7 @@ - + - + - + @@ -494,7 +498,7 @@ - - - + + + \ No newline at end of file