Compare commits

...

5 Commits
1.3.3 ... 1.3.5

Author SHA1 Message Date
DismissedLight
23f3e5df77 ContentDialogFactory 2023-01-09 12:15:11 +08:00
DismissedLight
4a027a8d3f remove filesystem context 2023-01-08 14:54:16 +08:00
DismissedLight
80459708a7 fix panel selector global group name 2023-01-08 12:32:43 +08:00
DismissedLight
650b67bea0 download static resource at startup 2023-01-07 18:27:45 +08:00
Masterain
18b3d23b1c Update azure-pipelines.yml for Azure Pipelines
- optimize CI logic for RPs
2023-01-03 19:17:14 -08:00
151 changed files with 2195 additions and 1462 deletions

View File

@@ -17,6 +17,16 @@ trigger:
- azure-pipelines.yml
- .github/ISSUE_TEMPLATE/*.yml
- .github/workflows/*.yml
pr:
branches:
include:
- main
paths:
exclude:
- README.md
- azure-pipelines.yml
- .github/ISSUE_TEMPLATE/*.yml
- .github/workflows/*.yml
pool:
@@ -134,6 +144,7 @@ steps:
secureFile: 'Snap.Hutao.CI.cer'
- task: GitHubRelease@1
condition: or(eq(variables['Build.Reason'], 'Manual'), eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'))
inputs:
gitHubConnection: 'github.com_Masterain'
repositoryName: 'DGP-Studio/Snap.Hutao'

View File

@@ -42,6 +42,23 @@
<CornerRadius x:Key="CompatCornerRadiusRight">0,6,6,0</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusBottom">0,0,6,6</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
<!-- OpenPaneLength -->
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
<x:Double x:Key="CompatSplitViewOpenPaneLength2">252</x:Double>
<GridLength x:Key="CompatGridLength2">252</GridLength>
<!-- Uris -->
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie</x:String>
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
<x:String x:Key="UI_ItemIcon_None">https://static.snapgenshin.com/Bg/UI_ItemIcon_None.png</x:String>
<x:String x:Key="UI_ImgSign_ItemIcon">https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png</x:String>
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://static.snapgenshin.com/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
<x:String x:Key="UI_EmotionIcon25">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon293.png</x:String>
<!-- Converters -->
<cwuc:BoolNegationConverter x:Key="BoolNegationConverter"/>
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
@@ -70,6 +87,8 @@
<shvc:EmptyObjectToBoolRevertConverter x:Key="EmptyObjectToBoolRevertConverter"/>
<shvc:EmptyObjectToVisibilityConverter x:Key="EmptyObjectToVisibilityConverter"/>
<shvc:EmptyObjectToVisibilityRevertConverter x:Key="EmptyObjectToVisibilityRevertConverter"/>
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
<!-- Styles -->
<Style
x:Key="LargeGridViewItemStyle"

View File

@@ -51,7 +51,7 @@ public partial class App : Application
ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate;
logger.LogInformation(EventIds.CommonLog, "Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.TemporaryFolder.Path);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.LocalCacheFolder.Path);
JumpListHelper.ConfigureAsync().SafeForget(logger);
}

View File

@@ -1,174 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Context.FileSystem.Location;
using System.IO;
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 文件系统上下文
/// </summary>
/// <typeparam name="TLocation">路径位置类型</typeparam>
internal abstract class FileSystemContext
{
private readonly IFileSystemLocation location;
/// <summary>
/// 初始化文件系统上下文
/// </summary>
/// <param name="location">指定的文件系统位置</param>
public FileSystemContext(IFileSystemLocation location)
{
this.location = location;
EnsureDirectory();
}
/// <summary>
/// 创建文件,若已存在文件,则不会创建
/// </summary>
/// <param name="file">文件</param>
public void CreateFileOrIgnore(string file)
{
file = Locate(file);
if (!File.Exists(file))
{
File.Create(file).Dispose();
}
}
/// <summary>
/// 创建文件夹,若已存在文件,则不会创建
/// </summary>
/// <param name="folder">文件夹</param>
public void CreateFolderOrIgnore(string folder)
{
folder = Locate(folder);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
}
/// <summary>
/// 尝试删除文件夹
/// </summary>
/// <param name="folder">文件夹</param>
public void DeleteFolderOrIgnore(string folder)
{
folder = Locate(folder);
if (Directory.Exists(folder))
{
Directory.Delete(folder, true);
}
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="file">文件名称</param>
/// <returns>是否存在</returns>
public bool FileExists(string file)
{
return File.Exists(Locate(file));
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="folder">文件夹名称</param>
/// <param name="file">文件名称</param>
/// <returns>是否存在</returns>
public bool FileExists(string folder, string file)
{
return File.Exists(Locate(folder, file));
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="folder">文件夹名称</param>
/// <returns>是否存在</returns>
public bool FolderExists(string folder)
{
return Directory.Exists(Locate(folder));
}
/// <summary>
/// 定位根目录中的文件或文件夹
/// </summary>
/// <param name="fileOrFolder">文件或文件夹</param>
/// <returns>绝对路径</returns>
public string Locate(string fileOrFolder)
{
return Path.GetFullPath(fileOrFolder, location.GetPath());
}
/// <summary>
/// 定位根目录下子文件夹中的文件
/// </summary>
/// <param name="folder">文件夹</param>
/// <param name="file">文件</param>
/// <returns>绝对路径</returns>
public string Locate(string folder, string file)
{
return Path.GetFullPath(Path.Combine(folder, file), location.GetPath());
}
/// <summary>
/// 将文件移动到指定的子目录
/// </summary>
/// <param name="file">文件</param>
/// <param name="folder">文件夹</param>
/// <param name="overwrite">是否覆盖</param>
/// <returns>是否成功 当文件不存在时会失败</returns>
public bool MoveToFolderOrIgnore(string file, string folder, bool overwrite = true)
{
string target = Locate(folder, file);
file = Locate(file);
if (File.Exists(file))
{
File.Move(file, target, overwrite);
return true;
}
return false;
}
/// <summary>
/// 等效于 <see cref="File.OpenRead(string)"/> ,但路径经过解析
/// </summary>
/// <param name="file">文件名</param>
/// <returns>文件流</returns>
public FileStream OpenRead(string file)
{
return File.OpenRead(Locate(file));
}
/// <summary>
/// 等效于 <see cref="File.Create(string)"/> ,但路径经过解析
/// </summary>
/// <param name="file">文件名</param>
/// <returns>文件流</returns>
public FileStream Create(string file)
{
return File.Create(Locate(file));
}
/// <summary>
/// 检查根目录
/// </summary>
/// <returns>是否创建了路径</returns>
private bool EnsureDirectory()
{
string folder = location.GetPath();
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
return true;
}
return false;
}
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 我的文档上下文
/// </summary>
[Injection(InjectAs.Transient)]
internal class HutaoContext : FileSystemContext
{
/// <inheritdoc cref="FileSystemContext"/>
public HutaoContext(Location.HutaoLocation myDocument)
: base(myDocument)
{
}
}

View File

@@ -1,35 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using Windows.ApplicationModel;
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 我的文档位置
/// </summary>
[Injection(InjectAs.Transient)]
internal class HutaoLocation : IFileSystemLocation
{
private string? path;
/// <inheritdoc/>
public string GetPath()
{
if (string.IsNullOrEmpty(path))
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
path = Path.GetFullPath(Path.Combine(myDocument, folderName));
}
return path;
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 文件系统位置
/// </summary>
public interface IFileSystemLocation
{
/// <summary>
/// 获取路径
/// </summary>
/// <returns>路径</returns>
string GetPath();
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 我的文档位置
/// </summary>
[Injection(InjectAs.Transient)]
internal class Metadata : IFileSystemLocation
{
private string? path;
/// <inheritdoc/>
public string GetPath()
{
if (string.IsNullOrEmpty(path))
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
path = Path.GetFullPath(Path.Combine(myDocument, "Hutao", "Metadata"));
}
return path;
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Context.FileSystem.Location;
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 元数据上下文
/// </summary>
[Injection(InjectAs.Transient)]
internal class MetadataContext : FileSystemContext
{
/// <inheritdoc cref="FileSystemContext"/>
public MetadataContext(Metadata metadata)
: base(metadata)
{
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Extension;
@@ -11,19 +10,6 @@ namespace Snap.Hutao.Control.Extension;
/// </summary>
internal static class ContentDialogExtensions
{
/// <summary>
/// 针对窗口进行初始化
/// </summary>
/// <param name="contentDialog">对话框</param>
/// <param name="window">窗口</param>
/// <returns>初始化完成的对话框</returns>
public static ContentDialog InitializeWithWindow(this ContentDialog contentDialog, Window window)
{
contentDialog.XamlRoot = window.Content.XamlRoot;
return contentDialog;
}
/// <summary>
/// 阻止用户交互
/// </summary>

View File

@@ -7,7 +7,6 @@ using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Extension;
using System.Runtime.InteropServices;
using Windows.Storage;
namespace Snap.Hutao.Control.Image;
@@ -23,6 +22,7 @@ public class CachedImage : ImageEx
{
IsCacheEnabled = true;
EnableLazyLoading = true;
LazyLoadingThreshold = 500;
}
/// <inheritdoc/>
@@ -33,18 +33,18 @@ public class CachedImage : ImageEx
try
{
Verify.Operation(imageUri.Host != string.Empty, "无效的Uri");
StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
// check token state to determine whether the operation should be canceled.
token.ThrowIfCancellationRequested();
// BitmapImage initialize with a uri will increase image quality and loading speed.
return new BitmapImage(new(file.Path));
return new BitmapImage(new(file));
}
catch (COMException)
{
// The image is corrupted, remove it.
await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false);
imageCache.Remove(imageUri.Enumerate());
return null;
}
catch (OperationCanceledException)

View File

@@ -152,19 +152,17 @@ internal static class CompositionExtensions
/// 创建一个线性渐变画刷
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="start">起点</param>
/// <param name="end">终点</param>
/// <param name="direction">方向</param>
/// <param name="stops">锚点</param>
/// <returns>线性渐变画刷</returns>
public static CompositionLinearGradientBrush CompositeLinearGradientBrush(
this Compositor compositor,
Vector2 start,
Vector2 end,
GradientDirection direction,
params GradientStop[] stops)
{
CompositionLinearGradientBrush brush = compositor.CreateLinearGradientBrush();
brush.StartPoint = start;
brush.EndPoint = end;
brush.StartPoint = GetStartPointOfDirection(direction);
brush.EndPoint = GetEndPointOfDirection(direction);
foreach (GradientStop stop in stops)
{
@@ -193,5 +191,31 @@ internal static class CompositionExtensions
return brush;
}
private static Vector2 GetStartPointOfDirection(GradientDirection direction)
{
return direction switch
{
GradientDirection.BottomToTop => Vector2.UnitY,
GradientDirection.LeftBottomToRightTop => Vector2.UnitY,
GradientDirection.RightBottomToLeftTop => Vector2.One,
GradientDirection.RightToLeft => Vector2.UnitX,
GradientDirection.RightTopToLeftBottom => Vector2.UnitX,
_ => Vector2.Zero,
};
}
private static Vector2 GetEndPointOfDirection(GradientDirection direction)
{
return direction switch
{
GradientDirection.LeftBottomToRightTop => Vector2.UnitX,
GradientDirection.LeftToRight => Vector2.UnitX,
GradientDirection.LeftTopToRightBottom => Vector2.One,
GradientDirection.RightTopToLeftBottom => Vector2.UnitY,
GradientDirection.TopToBottom => Vector2.UnitY,
_ => Vector2.Zero,
};
}
public record struct GradientStop(float Offset, Windows.UI.Color Color);
}

View File

@@ -9,10 +9,9 @@ using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using System.IO;
using System.Net.Http;
using System.Runtime.InteropServices;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image;
@@ -60,15 +59,16 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
/// <summary>
/// 异步加载图像表面
/// </summary>
/// <param name="storageFile">文件</param>
/// <param name="file">文件</param>
/// <param name="token">取消令牌</param>
/// <returns>加载的图像表面</returns>
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token).ConfigureAwait(true))
{
return LoadedImageSurface.StartLoadFromStream(imageStream);
}
TaskCompletionSource loadCompleteTaskSource = new();
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
await loadCompleteTaskSource.Task.ConfigureAwait(true);
return surface;
}
/// <summary>
@@ -130,7 +130,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
}
else
{
StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
string storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
try
{
@@ -138,7 +138,11 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
}
catch (COMException)
{
await imageCache.RemoveAsync(uri.Enumerate()).ConfigureAwait(true);
imageCache.Remove(uri.Enumerate());
}
catch (IOException)
{
imageCache.Remove(uri.Enumerate());
}
}

View File

@@ -3,10 +3,10 @@
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using System.Numerics;
using System.IO;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image;
@@ -16,8 +16,29 @@ namespace Snap.Hutao.Control.Image;
/// </summary>
public class Gradient : CompositionImage
{
private static readonly DependencyProperty BackgroundDirectionProperty = Property<Gradient>.Depend(nameof(BackgroundDirection), GradientDirection.TopToBottom);
private static readonly DependencyProperty ForegroundDirectionProperty = Property<Gradient>.Depend(nameof(ForegroundDirection), GradientDirection.TopToBottom);
private double imageAspectRatio;
/// <summary>
/// 背景方向
/// </summary>
public GradientDirection BackgroundDirection
{
get => (GradientDirection)GetValue(BackgroundDirectionProperty);
set => SetValue(BackgroundDirectionProperty, value);
}
/// <summary>
/// 前景方向
/// </summary>
public GradientDirection ForegroundDirection
{
get => (GradientDirection)GetValue(ForegroundDirectionProperty);
set => SetValue(ForegroundDirectionProperty, value);
}
/// <inheritdoc/>
protected override void OnUpdateVisual(SpriteVisual spriteVisual)
{
@@ -29,15 +50,22 @@ public class Gradient : CompositionImage
}
/// <inheritdoc/>
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token).ConfigureAwait(true))
using (FileStream fileStream = new(file, FileMode.Open, FileAccess.Read, FileShare.Read))
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token).ConfigureAwait(true);
imageAspectRatio = decoder.PixelWidth / (double)decoder.PixelHeight;
return LoadedImageSurface.StartLoadFromStream(imageStream);
using (IRandomAccessStream imageStream = fileStream.AsRandomAccessStream())
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream);
imageAspectRatio = decoder.PixelWidth / (double)decoder.PixelHeight;
}
}
TaskCompletionSource loadCompleteTaskSource = new();
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
await loadCompleteTaskSource.Task.ConfigureAwait(true);
return surface;
}
/// <inheritdoc/>
@@ -45,8 +73,8 @@ public class Gradient : CompositionImage
{
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));
CompositionLinearGradientBrush backgroundBrush = compositor.CompositeLinearGradientBrush(BackgroundDirection, new(0, Colors.White), new(1, Colors.Black));
CompositionLinearGradientBrush foregroundBrush = compositor.CompositeLinearGradientBrush(ForegroundDirection, new(0, Colors.White), new(1, Colors.Black));
CompositionEffectBrush gradientEffectBrush = compositor.CompositeBlendEffectBrush(backgroundBrush, foregroundBrush);
CompositionEffectBrush opacityMaskEffectBrush = compositor.CompositeLuminanceToAlphaEffectBrush(gradientEffectBrush);

View File

@@ -0,0 +1,50 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Image;
/// <summary>
/// 渐变方向
/// </summary>
public enum GradientDirection
{
/// <summary>
/// 下到上
/// </summary>
BottomToTop,
/// <summary>
/// 左下到右上
/// </summary>
LeftBottomToRightTop,
/// <summary>
/// 左到右
/// </summary>
LeftToRight,
/// <summary>
/// 左上到右下
/// </summary>
LeftTopToRightBottom,
/// <summary>
/// 右下到左上
/// </summary>
RightBottomToLeftTop,
/// <summary>
/// 右到左
/// </summary>
RightToLeft,
/// <summary>
/// 右上到左下
/// </summary>
RightTopToLeftBottom,
/// <summary>
/// 上到下
/// </summary>
TopToBottom,
}

View File

@@ -1,31 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.Control.Markup;
/// <summary>
/// Uri扩展
/// </summary>
[MarkupExtensionReturnType(ReturnType = typeof(Uri))]
public sealed class UriExtension : MarkupExtension
{
/// <summary>
/// 构造一个新的Uri扩展
/// </summary>
public UriExtension()
{
}
/// <summary>
/// 地址
/// </summary>
public string? Value { get; set; }
/// <inheritdoc/>
protected override object ProvideValue()
{
return new Uri(Value ?? string.Empty);
}
}

View File

@@ -5,12 +5,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
Loaded="OnRootControlLoaded"
mc:Ignorable="d">
<SplitButton
Name="RootSplitButton"
Padding="0,6"
Click="SplitButtonClick"
Loaded="SplitButtonLoaded">
Click="SplitButtonClick">
<SplitButton.Content>
<FontIcon Name="IconPresenter" Glyph="&#xE8FD;"/>
</SplitButton.Content>

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Control.Panel;
/// </summary>
public sealed partial class PanelSelector : UserControl
{
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List");
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List", OnCurrentChanged);
/// <summary>
/// 构造一个新的面板选择器
@@ -30,51 +30,60 @@ public sealed partial class PanelSelector : UserControl
set => SetValue(CurrentProperty, value);
}
private void SplitButtonLoaded(object sender, RoutedEventArgs e)
private static void OnCurrentChanged(PanelSelector sender, string current)
{
MenuFlyout menuFlyout = (MenuFlyout)((SplitButton)sender).Flyout;
((RadioMenuFlyoutItem)menuFlyout.Items[0]).IsChecked = true;
MenuFlyout menuFlyout = (MenuFlyout)sender.RootSplitButton.Flyout;
RadioMenuFlyoutItem targetItem = menuFlyout.Items
.Cast<RadioMenuFlyoutItem>()
.Single(i => (string)i.Tag == current);
targetItem.IsChecked = true;
sender.IconPresenter.Glyph = ((FontIcon)targetItem.Icon).Glyph;
}
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
OnCurrentChanged((PanelSelector)obj, (string)args.NewValue);
}
private void OnRootControlLoaded(object sender, RoutedEventArgs e)
{
// because the GroupName shares in global
// we have to impl a control scoped GroupName.
PanelSelector selector = (PanelSelector)sender;
MenuFlyout menuFlyout = (MenuFlyout)selector.RootSplitButton.Flyout;
int hash = GetHashCode();
foreach (RadioMenuFlyoutItem item in menuFlyout.Items.Cast<RadioMenuFlyoutItem>())
{
item.GroupName = $"PanelSelector{hash}Group";
}
OnCurrentChanged(selector, Current);
}
private void SplitButtonClick(SplitButton sender, SplitButtonClickEventArgs args)
{
MenuFlyout menuFlyout = (MenuFlyout)sender.Flyout;
int i = 0;
for (; i < menuFlyout.Items.Count; i++)
{
RadioMenuFlyoutItem current = (RadioMenuFlyoutItem)menuFlyout.Items[i];
if (current.IsChecked)
if ((string)menuFlyout.Items[i].Tag == Current)
{
break;
}
}
i++;
if (i > menuFlyout.Items.Count)
{
i = 1;
}
if (i == menuFlyout.Items.Count)
{
i = 0;
}
++i;
i %= menuFlyout.Items.Count; // move the count index to 0
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)menuFlyout.Items[i];
item.IsChecked = true;
UpdateState(item);
Current = (string)item.Tag;
}
private void RadioMenuFlyoutItemClick(object sender, RoutedEventArgs e)
{
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)sender;
UpdateState(item);
}
private void UpdateState(RadioMenuFlyoutItem item)
{
Current = (string)item.Tag;
IconPresenter.Glyph = ((FontIcon)item.Icon).Glyph;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
[SuppressMessage("", "SA1600")]
public abstract class DisposableObject : IDisposable
{
public bool IsDisposed { get; private set; }
public void Dispose()
{
if (!IsDisposed)
{
GC.SuppressFinalize(this);
Dispose(isDisposing: true);
}
}
protected virtual void Dispose(bool isDisposing)
{
IsDisposed = true;
}
protected void VerifyNotDisposed()
{
if (IsDisposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
}
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Storage;
namespace Snap.Hutao.Core.Caching;
/// <summary>
@@ -12,23 +10,20 @@ namespace Snap.Hutao.Core.Caching;
internal interface IImageCache
{
/// <summary>
/// Gets the StorageFile containing cached item for given Uri
/// Gets the file path containing cached item for given Uri
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>a StorageFile</returns>
Task<StorageFile> GetFileFromCacheAsync(Uri uri);
/// <returns>a string path</returns>
Task<string> GetFileFromCacheAsync(Uri uri);
/// <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);
void Remove(IEnumerable<Uri> uriForCachedItems);
/// <summary>
/// Removes cached files that have expired
/// Removes invalid cached files
/// </summary>
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
/// <returns>awaitable task</returns>
Task RemoveExpiredAsync(TimeSpan? duration = null);
void RemoveInvalid();
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Caching;
/// <summary>
/// 图像缓存 文件路径操作
/// </summary>
internal interface IImageCacheFilePathOperation
{
/// <summary>
/// 从分类与文件名获取文件路径
/// </summary>
/// <param name="category">分类</param>
/// <param name="fileName">文件名</param>
/// <returns>文件路径</returns>
string GetFilePathFromCategoryAndFileName(string category, string fileName);
}

View File

@@ -10,7 +10,6 @@ using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Windows.Storage;
using Windows.Storage.FileProperties;
namespace Snap.Hutao.Core.Caching;
@@ -20,11 +19,10 @@ namespace Snap.Hutao.Core.Caching;
/// </summary>
[Injection(InjectAs.Singleton, typeof(IImageCache))]
[HttpClient(HttpClientConfigration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 16)]
[SuppressMessage("", "CA1001")]
public class ImageCache : IImageCache
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
public class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string DateAccessedProperty = "System.DateAccessed";
private const string CacheFolderName = nameof(ImageCache);
private static readonly ImmutableDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
{
@@ -36,17 +34,13 @@ public class ImageCache : IImageCache
[5] = TimeSpan.FromSeconds(64),
}.ToImmutableDictionary();
private readonly List<string> extendedPropertyNames = new() { DateAccessedProperty };
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
private readonly ILogger logger;
// violate di rule
private readonly HttpClient httpClient;
private StorageFolder? baseFolder;
private string? cacheFolderName;
private StorageFolder? cacheFolder;
private string? baseFolder;
private string? cacheFolder;
/// <summary>
/// Initializes a new instance of the <see cref="ImageCache"/> class.
@@ -57,115 +51,84 @@ public class ImageCache : IImageCache
{
this.logger = logger;
httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
CacheDuration = TimeSpan.FromDays(30);
RetryCount = 3;
}
/// <summary>
/// Gets or sets the life duration of every cache entry.
/// </summary>
public TimeSpan CacheDuration { get; }
/// <summary>
/// Gets or sets the number of retries trying to ensure the file is cached.
/// </summary>
public uint RetryCount { get; }
/// <summary>
/// Clears all files in the cache
/// </summary>
/// <returns>awaitable task</returns>
public async Task ClearAsync()
/// <inheritdoc/>
public void RemoveInvalid()
{
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
string folder = GetCacheFolder();
string[] files = Directory.GetFiles(folder);
await RemoveAsync(files).ConfigureAwait(false);
}
List<string> filesToDelete = new();
/// <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;
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
List<StorageFile> filesToDelete = new();
foreach (StorageFile file in files)
foreach (string file in files)
{
if (file == null)
{
continue;
}
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false))
if (IsFileInvalid(file, false))
{
filesToDelete.Add(file);
}
}
await RemoveAsync(filesToDelete).ConfigureAwait(false);
RemoveInternal(filesToDelete);
}
/// <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)
/// <inheritdoc/>
public void Remove(IEnumerable<Uri> uriForCachedItems)
{
if (uriForCachedItems == null || !uriForCachedItems.Any())
{
return;
}
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
string folder = GetCacheFolder();
string[] files = Directory.GetFiles(folder);
List<StorageFile> filesToDelete = new();
Dictionary<string, StorageFile> cachedFiles = files.ToDictionary(file => file.Name);
List<string> filesToDelete = new();
foreach (Uri uri in uriForCachedItems)
{
string fileName = GetCacheFileName(uri);
if (cachedFiles.TryGetValue(fileName, out StorageFile? file))
string filePath = Path.Combine(folder, GetCacheFileName(uri));
if (files.Contains(filePath))
{
filesToDelete.Add(file);
filesToDelete.Add(filePath);
}
}
await RemoveAsync(filesToDelete).ConfigureAwait(false);
RemoveInternal(filesToDelete);
}
/// <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)
/// <inheritdoc/>
public async Task<string> GetFileFromCacheAsync(Uri uri)
{
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
string filePath = Path.Combine(GetCacheFolder(), GetCacheFileName(uri));
string fileName = GetCacheFileName(uri);
IStorageItem? item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
if (item == null || (await item.GetBasicPropertiesAsync()).Size == 0)
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
{
StorageFile baseFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
await DownloadFileAsync(uri, baseFile).ConfigureAwait(false);
item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
}
return Must.NotNull((item as StorageFile)!);
return filePath;
}
/// <inheritdoc/>
public string GetFilePathFromCategoryAndFileName(string category, string fileName)
{
Uri dummyUri = new(Web.HutaoEndpoints.StaticFile(category, fileName));
return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri));
}
private static void RemoveInternal(IEnumerable<string> filePaths)
{
foreach (string filePath in filePaths)
{
try
{
File.Delete(filePath);
}
catch
{
}
}
}
private static string GetCacheFileName(Uri uri)
@@ -176,48 +139,19 @@ public class ImageCache : IImageCache
return System.Convert.ToHexString(hash);
}
/// <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>
private async Task<bool> IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true)
private static bool IsFileInvalid(string file, bool treatNullFileAsInvalid = true)
{
if (file == null)
if (!File.Exists(file))
{
return treatNullFileAsOutOfDate;
return treatNullFileAsInvalid;
}
// 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;
FileInfo fileInfo = new(file);
return fileInfo.Length == 0;
}
private async Task DownloadFileAsync(Uri uri, StorageFile baseFile)
private async Task DownloadFileAsync(Uri uri, string baseFile)
{
logger.LogInformation(EventIds.FileCaching, "Begin downloading for {uri}", uri);
@@ -230,18 +164,23 @@ public class ImageCache : IImageCache
{
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
using (FileStream fileStream = File.Create(baseFile.Path))
using (FileStream fileStream = File.Create(baseFile))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
}
}
else if (message.StatusCode == HttpStatusCode.NotFound)
{
// directly goto https://static.hut.ao
retryCount = 3;
}
else if (message.StatusCode == HttpStatusCode.TooManyRequests)
{
retryCount++;
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
logger.LogInformation("Retry after {delay}.", delay);
logger.LogInformation("Retry {uri} after {delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
}
else
@@ -252,61 +191,20 @@ public class ImageCache : IImageCache
if (retryCount == 3)
{
uri = new UriBuilder(uri) { Host = "static.hut.ao", }.Uri;
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao, }.Uri;
}
}
}
/// <summary>
/// Initializes with default values if user has not initialized explicitly
/// </summary>
/// <returns>awaitable task</returns>
private async Task InitializeInternalAsync()
{
if (cacheFolder != null)
{
return;
}
using (await cacheFolderSemaphore.EnterAsync().ConfigureAwait(false))
{
baseFolder ??= ApplicationData.Current.TemporaryFolder;
if (string.IsNullOrWhiteSpace(cacheFolderName))
{
cacheFolderName = GetType().Name;
}
cacheFolder = await baseFolder
.CreateFolderAsync(cacheFolderName, CreationCollisionOption.OpenIfExists)
.AsTask()
.ConfigureAwait(false);
}
}
private async Task<StorageFolder> GetCacheFolderAsync()
private string GetCacheFolder()
{
if (cacheFolder == null)
{
await InitializeInternalAsync().ConfigureAwait(false);
baseFolder ??= ApplicationData.Current.LocalCacheFolder.Path;
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
cacheFolder = info.FullName;
}
return Must.NotNull(cacheFolder!);
}
private async Task RemoveAsync(IEnumerable<StorageFile> files)
{
foreach (StorageFile file in files)
{
try
{
logger.LogInformation(EventIds.CacheRemoveFile, "Removing file {file}", file.Path);
await file.DeleteAsync().AsTask().ConfigureAwait(false);
}
catch
{
logger.LogError(EventIds.CacheException, "Failed to delete file: {file}", file.Path);
}
}
return cacheFolder!;
}
}

View File

@@ -7,6 +7,7 @@ using Snap.Hutao.Core.Json;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Collections.Immutable;
using System.IO;
using System.Text.Json.Serialization.Metadata;
using Windows.ApplicationModel;
@@ -70,6 +71,11 @@ internal static class CoreEnvironment
/// </summary>
public static readonly string FamilyName;
/// <summary>
/// 数据文件夹
/// </summary>
public static readonly string DataFolder;
/// <summary>
/// 默认的Json序列化选项
/// </summary>
@@ -93,6 +99,7 @@ internal static class CoreEnvironment
static CoreEnvironment()
{
DataFolder = GetDocumentsHutaoPath();
Version = Package.Current.Id.Version.ToVersion();
FamilyName = Package.Current.Id.FamilyName;
CommonUA = $"Snap Hutao/{Version}";
@@ -108,4 +115,19 @@ internal static class CoreEnvironment
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
return Md5Convert.ToHexString($"{userName}{machineGuid}");
}
private static string GetDocumentsHutaoPath()
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
Directory.CreateDirectory(path);
return path;
}
}

View File

@@ -3,7 +3,6 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Model.Entity.Database;
using System.Diagnostics;
@@ -31,9 +30,7 @@ internal static class IocConfiguration
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddDatebase(this IServiceCollection services)
{
HutaoContext myDocument = new(new());
string dbFile = myDocument.Locate("Userdata.db");
string dbFile = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
// temporarily create a context

View File

@@ -0,0 +1,311 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core.Abstraction;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Networking.BackgroundIntelligentTransferService;
namespace Snap.Hutao.Core.IO.Bits;
/// <summary>
/// BITS Job
/// </summary>
[SuppressMessage("", "SA1600")]
internal class BitsJob : DisposableObject, IBackgroundCopyCallback
{
private const uint BitsEngineNoProgressTimeout = 120;
private const int MaxResumeAttempts = 10;
private readonly string displayName;
private readonly ILogger<BitsJob> log;
private readonly object lockObj = new();
private IBackgroundCopyJob? nativeJob;
private System.Exception? jobException;
private BG_JOB_PROGRESS progress;
private BG_JOB_STATE state;
private bool isJobComplete;
private int resumeAttempts;
private BitsJob(IServiceProvider serviceProvider, string displayName, IBackgroundCopyJob job)
{
this.displayName = displayName;
nativeJob = job;
log = serviceProvider.GetRequiredService<ILogger<BitsJob>>();
}
public HRESULT ErrorCode { get; private set; }
public static BitsJob CreateJob(IServiceProvider serviceProvider, IBackgroundCopyManager backgroundCopyManager, Uri uri, string filePath)
{
ILogger<BitsJob> service = serviceProvider.GetRequiredService<ILogger<BitsJob>>();
string text = $"BitsDownloadJob - {uri}";
IBackgroundCopyJob ppJob;
try
{
backgroundCopyManager.CreateJob(text, BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out Guid _, out ppJob);
ppJob.SetNotifyFlags(11u);
ppJob.SetNoProgressTimeout(BitsEngineNoProgressTimeout);
ppJob.SetPriority(BG_JOB_PRIORITY.BG_JOB_PRIORITY_FOREGROUND);
ppJob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_AUTODETECT, null, null);
}
catch (COMException ex)
{
service.LogInformation("Failed to create job. {message}", ex.Message);
throw;
}
BitsJob bitsJob = new(serviceProvider, text, ppJob);
bitsJob.InitJob(uri.AbsoluteUri, filePath);
return bitsJob;
}
public void JobTransferred(IBackgroundCopyJob job)
{
try
{
UpdateProgress();
UpdateJobState();
CompleteOrCancel();
}
catch (System.Exception ex)
{
log.LogInformation("Failed to job transfer: {message}", ex.Message);
}
}
public void JobError(IBackgroundCopyJob job, IBackgroundCopyError error)
{
IBackgroundCopyError error2 = error;
try
{
log.LogInformation("Failed job: {message}", displayName);
UpdateJobState();
BG_ERROR_CONTEXT errorContext = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_NONE;
HRESULT returnCode = new(0);
Invoke(() => error2.GetError(out errorContext, out returnCode), "GetError", throwOnFailure: false);
ErrorCode = returnCode;
jobException = new IOException(string.Format("Error context: {0}, Error code: {1}", errorContext, returnCode));
CompleteOrCancel();
log.LogInformation(jobException, "Job Exception:");
}
catch (System.Exception ex)
{
log?.LogInformation("Failed to handle job error: {message}", ex.Message);
}
}
public void JobModification(IBackgroundCopyJob job, uint reserved)
{
try
{
UpdateJobState();
if (state == BG_JOB_STATE.BG_JOB_STATE_TRANSIENT_ERROR)
{
HRESULT errorCode = GetErrorCode(job);
if (errorCode == -2145844944)
{
ErrorCode = errorCode;
CompleteOrCancel();
return;
}
resumeAttempts++;
if (resumeAttempts <= MaxResumeAttempts)
{
Resume();
return;
}
log.LogInformation("Max resume attempts for job '{name}' exceeded. Canceling.", displayName);
CompleteOrCancel();
}
else if (IsProgressingState(state))
{
UpdateProgress();
}
else if (state == BG_JOB_STATE.BG_JOB_STATE_CANCELLED || state == BG_JOB_STATE.BG_JOB_STATE_ERROR)
{
CompleteOrCancel();
}
}
catch (System.Exception ex)
{
log.LogInformation(ex, "message");
}
}
public void Cancel()
{
log.LogInformation("Canceling job {name}", displayName);
lock (lockObj)
{
if (!isJobComplete)
{
Invoke(() => nativeJob?.Cancel(), "Bits Cancel");
jobException = new OperationCanceledException();
isJobComplete = true;
}
}
}
public void WaitForCompletion(Action<ProgressUpdateStatus> callback, CancellationToken cancellationToken)
{
CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(Cancel);
int noProgressSeconds = 0;
try
{
UpdateJobState();
while (IsProgressingState(state) || state == BG_JOB_STATE.BG_JOB_STATE_QUEUED)
{
if (noProgressSeconds > BitsEngineNoProgressTimeout)
{
jobException = new TimeoutException($"Timeout reached for job {displayName} whilst in state {state}");
break;
}
cancellationToken.ThrowIfCancellationRequested();
UpdateJobState();
UpdateProgress();
if (state is BG_JOB_STATE.BG_JOB_STATE_TRANSFERRING or BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED or BG_JOB_STATE.BG_JOB_STATE_ACKNOWLEDGED)
{
noProgressSeconds = 0;
callback(new ProgressUpdateStatus((long)progress.BytesTransferred, (long)progress.BytesTotal));
}
// Refresh every seconds.
Thread.Sleep(1000);
++noProgressSeconds;
}
}
finally
{
cancellationTokenRegistration.Dispose();
CompleteOrCancel();
}
if (jobException != null)
{
throw jobException;
}
}
protected override void Dispose(bool isDisposing)
{
UpdateJobState();
CompleteOrCancel();
nativeJob = null;
base.Dispose(isDisposing);
}
private static bool IsProgressingState(BG_JOB_STATE state)
{
if (state != BG_JOB_STATE.BG_JOB_STATE_CONNECTING && state != BG_JOB_STATE.BG_JOB_STATE_TRANSIENT_ERROR)
{
return state == BG_JOB_STATE.BG_JOB_STATE_TRANSFERRING;
}
return true;
}
private void CompleteOrCancel()
{
if (isJobComplete)
{
return;
}
lock (lockObj)
{
if (isJobComplete)
{
return;
}
if (state == BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED)
{
log.LogInformation("Completing job '{name}'.", displayName);
Invoke(() => nativeJob?.Complete(), "Bits Complete");
while (state == BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED)
{
Thread.Sleep(50);
UpdateJobState();
}
}
else
{
log.LogInformation("Canceling job '{name}'.", displayName);
Invoke(() => nativeJob?.Cancel(), "Bits Cancel");
}
isJobComplete = true;
}
}
private void UpdateJobState()
{
if (nativeJob is IBackgroundCopyJob job)
{
Invoke(() => job.GetState(out state), "GetState");
}
}
private void UpdateProgress()
{
if (!isJobComplete)
{
Invoke(() => nativeJob?.GetProgress(out progress), "GetProgress");
}
}
private void Resume()
{
Invoke(() => nativeJob?.Resume(), "Bits Resume");
}
private void Invoke(Action action, string displayName, bool throwOnFailure = true)
{
try
{
action();
}
catch (System.Exception ex)
{
log.LogInformation("{name} failed. {exception}", displayName, ex);
if (throwOnFailure)
{
throw;
}
}
}
private void InitJob(string remoteUrl, string filePath)
{
nativeJob?.AddFile(remoteUrl, filePath);
nativeJob?.SetNotifyInterface(this);
Resume();
}
private HRESULT GetErrorCode(IBackgroundCopyJob job)
{
IBackgroundCopyJob job2 = job;
IBackgroundCopyError? error = null;
Invoke(() => job2.GetError(out error), "GetError", false);
if (error != null)
{
HRESULT returnCode = new(0);
Invoke(() => error.GetError(out _, out returnCode), "GetError", false);
return returnCode;
}
return new(0);
}
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Windows.Win32.Networking.BackgroundIntelligentTransferService;
namespace Snap.Hutao.Core.IO.Bits;
/// <summary>
/// BITS 管理器
/// </summary>
[Injection(InjectAs.Singleton)]
internal class BitsManager
{
private readonly Lazy<IBackgroundCopyManager> lazyBackgroundCopyManager = new(() => (IBackgroundCopyManager)new BackgroundCopyManager());
private readonly IServiceProvider serviceProvider;
private readonly ILogger<BitsManager> logger;
/// <summary>
/// 构造一个新的 BITS 管理器
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public BitsManager(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
logger = serviceProvider.GetRequiredService<ILogger<BitsManager>>();
}
/// <summary>
/// 异步下载文件
/// </summary>
/// <param name="uri">文件uri</param>
/// <param name="progress">进度</param>
/// <param name="token">取消令牌</param>
/// <returns>是否下载成功,以及创建的文件</returns>
public async Task<ValueResult<bool, TempFile>> DownloadAsync(Uri uri, IProgress<ProgressUpdateStatus> progress, CancellationToken token = default)
{
TempFile tempFile = new(true);
bool result = await Task.Run(() => DownloadCore(uri, tempFile.Path, progress.Report, token), token).ConfigureAwait(false);
return new(result, tempFile);
}
private bool DownloadCore(Uri uri, string tempFile, Action<ProgressUpdateStatus> progress, CancellationToken token)
{
IBackgroundCopyManager value;
try
{
value = lazyBackgroundCopyManager.Value;
}
catch (System.Exception ex)
{
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
return false;
}
using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
{
try
{
bitsJob.WaitForCompletion(progress, token);
}
catch (System.Exception ex)
{
logger?.LogWarning(ex, "BITS download failed:");
return false;
}
if (bitsJob.ErrorCode != 0)
{
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.IO.Bits;
/// <summary>
/// 进度更新状态
/// </summary>
public class ProgressUpdateStatus
{
/// <summary>
/// 构造一个新的进度更新状态
/// </summary>
/// <param name="bytesRead">接收字节数</param>
/// <param name="totalBytes">总字节数</param>
public ProgressUpdateStatus(long bytesRead, long totalBytes)
{
BytesRead = bytesRead;
TotalBytes = totalBytes;
}
/// <summary>
/// 接收字节数
/// </summary>
public long BytesRead { get; private set; }
/// <summary>
/// 总字节数
/// </summary>
public long TotalBytes { get; private set; }
/// <inheritdoc/>
public override string ToString()
{
return $"{BytesRead}/{TotalBytes}";
}
}

View File

@@ -8,14 +8,20 @@ namespace Snap.Hutao.Core.IO;
/// <summary>
/// 封装一个临时文件
/// </summary>
internal sealed class TemporaryFile : IDisposable
internal sealed class TempFile : IDisposable
{
/// <summary>
/// 构造一个新的临时文件
/// </summary>
public TemporaryFile()
/// <param name="delete">是否在创建时删除文件</param>
public TempFile(bool delete = false)
{
Path = System.IO.Path.GetTempFileName();
if (delete)
{
File.Delete(Path);
}
}
/// <summary>
@@ -28,9 +34,9 @@ internal sealed class TemporaryFile : IDisposable
/// </summary>
/// <param name="file">源文件</param>
/// <returns>临时文件</returns>
public static TemporaryFile? CreateFromFileCopy(string file)
public static TempFile? CreateFromFileCopy(string file)
{
TemporaryFile temporaryFile = new();
TempFile temporaryFile = new();
try
{
File.Copy(file, temporaryFile.Path, true);

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.WinUI.Notifications;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.DailyNote;
@@ -123,6 +124,9 @@ internal static class Activation
{
case "":
{
// Increase launch times
LocalSetting.Set(SettingKeys.LaunchTimes, LocalSetting.Get(SettingKeys.LaunchTimes, 0) + 1);
await WaitMainWindowAsync().ConfigureAwait(false);
break;
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.Concurrent;
using System.Diagnostics;
@@ -61,8 +60,8 @@ public sealed class LogEntryQueue : IDisposable
private static LogDbContext InitializeDbContext()
{
HutaoContext myDocument = new(new());
LogDbContext logDbContext = LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}");
string logDbName = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Log.db");
LogDbContext logDbContext = LogDbContext.Create($"Data Source={logDbName}");
if (logDbContext.Database.GetPendingMigrations().Any())
{
Debug.WriteLine("[Debug] Performing LogDbContext Migrations");
@@ -70,7 +69,7 @@ public sealed class LogEntryQueue : IDisposable
}
// only raw sql can pass
logDbContext.Database.ExecuteSqlRaw("DELETE FROM logs WHERE Exception IS NULL");
logDbContext.Logs.Where(log => log.Exception == null).ExecuteDelete();
return logDbContext;
}

View File

@@ -17,4 +17,19 @@ internal static class SettingKeys
/// 导航侧栏是否展开
/// </summary>
public const string IsNavPaneOpen = "IsNavPaneOpen";
/// <summary>
/// 启动次数
/// </summary>
public const string LaunchTimes = "LaunchTimes";
/// <summary>
/// 静态资源合约V1
/// </summary>
public const string StaticResourceV1Contract = "StaticResourceV1Contract";
/// <summary>
/// 静态资源合约V2 成就图标与物品图标
/// </summary>
public const string StaticResourceV2Contract = "StaticResourceV2Contract";
}

View File

@@ -77,4 +77,32 @@ public static partial class EnumerableExtension
return false;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
public static Dictionary<TKey, TSource> ToDictionaryOverride<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
where TKey : notnull
{
Dictionary<TKey, TSource> dictionary = new();
foreach (TSource value in source)
{
dictionary[keySelector(value)] = value;
}
return dictionary;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
public static Dictionary<TKey, TValue> ToDictionaryOverride<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> valueSelector)
where TKey : notnull
{
Dictionary<TKey, TValue> dictionary = new();
foreach (TSource value in source)
{
dictionary[keySelector(value)] = valueSelector(value);
}
return dictionary;
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.ObjectModel;
namespace Snap.Hutao.Extension;
/// <summary>
@@ -8,26 +10,6 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static partial class EnumerableExtension
{
/// <summary>
/// 计数
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <typeparam name="TKey">计数的键类型</typeparam>
/// <param name="source">源</param>
/// <param name="keySelector">键选择器</param>
/// <returns>计数表</returns>
public static IEnumerable<KeyValuePair<TKey, int>> CountBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
where TKey : notnull, IEquatable<TKey>
{
CounterInt32<TKey> counter = new();
foreach (TSource item in source)
{
counter.Increase(keySelector(item));
}
return counter;
}
/// <summary>
/// 如果传入集合不为空则原路返回,
/// 如果传入集合为空返回一个集合的空集
@@ -64,56 +46,14 @@ public static partial class EnumerableExtension
return source.FirstOrDefault(predicate) ?? source.FirstOrDefault();
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
public static Dictionary<TKey, TSource> ToDictionaryOverride<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
where TKey : notnull
{
Dictionary<TKey, TSource> dictionary = new();
foreach (TSource value in source)
{
dictionary[keySelector(value)] = value;
}
return dictionary;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
public static Dictionary<TKey, TValue> ToDictionaryOverride<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> valueSelector)
where TKey : notnull
{
Dictionary<TKey, TValue> dictionary = new();
foreach (TSource value in source)
{
dictionary[keySelector(value)] = valueSelector(value);
}
return dictionary;
}
/// <summary>
/// 表示一个对 <see cref="TItem"/> 类型的计数器
/// 转换到 <see cref="ObservableCollection{T}"/>
/// </summary>
/// <typeparam name="TItem">待计数的类型</typeparam>
private class CounterInt32<TItem> : Dictionary<TItem, int>
where TItem : notnull, IEquatable<TItem>
/// <typeparam name="T">类型</typeparam>
/// <param name="source">源</param>
/// <returns><see cref="ObservableCollection{T}"/></returns>
public static ObservableCollection<T> ToObservableCollection<T>(this IEnumerable<T> source)
{
/// <summary>
/// 增加计数器
/// </summary>
/// <param name="item">物品</param>
public void Increase(TItem? item)
{
if (item != null)
{
if (!ContainsKey(item))
{
this[item] = 0;
}
this[item] += 1;
}
}
return new ObservableCollection<T>(source);
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Factory.Abstraction;
/// <summary>
/// 内容对话框工厂
/// </summary>
internal interface IContentDialogFactory
{
/// <summary>
/// 创建一个新的内容对话框,用于确认
/// </summary>
/// <param name="title">标题</param>
/// <param name="content">内容</param>
/// <returns>内容对话框</returns>
ContentDialog CreateForConfirm(string title, string content);
/// <summary>
/// 创建一个新的内容对话框,用于确认或取消
/// </summary>
/// <param name="title">标题</param>
/// <param name="content">内容</param>
/// <param name="defaultButton">默认按钮</param>
/// <returns>内容对话框</returns>
ContentDialog CreateForConfirmCancel(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close);
/// <summary>
/// 创建一个新的内容对话框,用于提示未知的进度
/// </summary>
/// <param name="title">标题</param>
/// <returns>内容对话框</returns>
ContentDialog CreateForIndeterminateProgress(string title);
}

View File

@@ -0,0 +1,67 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Factory.Abstraction;
namespace Snap.Hutao.Factory;
/// <inheritdoc cref="IContentDialogFactory"/>
[Injection(InjectAs.Transient, typeof(IContentDialogFactory))]
internal class ContentDialogFactory : IContentDialogFactory
{
private readonly MainWindow mainWindow;
/// <summary>
/// 构造一个新的内容对话框工厂
/// </summary>
/// <param name="mainWindow">主窗体</param>
public ContentDialogFactory(MainWindow mainWindow)
{
this.mainWindow = mainWindow;
}
/// <inheritdoc/>
public ContentDialog CreateForConfirm(string title, string content)
{
ContentDialog dialog = new()
{
XamlRoot = mainWindow.Content.XamlRoot,
Title = title,
Content = content,
DefaultButton = ContentDialogButton.Primary,
PrimaryButtonText = "确认",
};
return dialog;
}
/// <inheritdoc/>
public ContentDialog CreateForConfirmCancel(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
{
ContentDialog dialog = new()
{
XamlRoot = mainWindow.Content.XamlRoot,
Title = title,
Content = content,
DefaultButton = defaultButton,
PrimaryButtonText = "确认",
CloseButtonText = "取消",
};
return dialog;
}
/// <inheritdoc/>
public ContentDialog CreateForIndeterminateProgress(string title)
{
ContentDialog dialog = new()
{
XamlRoot = mainWindow.Content.XamlRoot,
Title = title,
Content = new ProgressBar() { IsIndeterminate = true },
};
return dialog;
}
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Localization;
/// <summary>
/// 翻译
/// </summary>
internal interface ITranslation
{
/// <summary>
/// 获取对应键的值
/// </summary>
/// <param name="key">键</param>
/// <returns>对应的值</returns>
string this[string key] { get; }
}

View File

@@ -1,111 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Localization;
/// <summary>
/// 中文翻译 zh-CN
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal class LanguagezhCN : ITranslation
{
private readonly Dictionary<string, string> translations = new()
{
["AppName"] = "胡桃",
["NavigationViewItem_Activity"] = "活动",
["NavigationViewItem_Achievement"] = "成就",
["NavigationViewItem_Wiki_Avatar"] = "角色",
["NavigationViewItem_GachaLog"] = "祈愿记录",
["UserPanel_Account"] = "账号",
["UserPanel_Add_Account"] = "添加新账号",
["UserPanel_GameRole"] = "角色",
["Achievement_Search_PlaceHolder"] = "搜索成就名称,描述或编号",
["Achievement_Create_Archive"] = "创建新存档",
["Achievement_Delete_Archive"] = "删除当前存档",
["Achievement_Import"] = "导入",
["Achievement_Import_From_Clipboard"] = "从剪贴板导入",
["Achievement_Import_From_File"] = "从 UIAF 文件导入",
["Achievement_IncompleteItemFirst"] = "优先未完成",
["Wiki_Avatar_Filter"] = "筛选",
["Wiki_Avatar_Filter_Element"] = "元素",
["Wiki_Avatar_Filter_Association"] = "所属",
["Wiki_Avatar_Filter_Weapon"] = "武器",
["Wiki_Avatar_Filter_Quality"] = "星级",
["Wiki_Avatar_Filter_Body"] = "体型",
["Wiki_Avatar_Fetter_Native"] = "所属",
["Wiki_Avatar_Fetter_Constellation"] = "命之座",
["Wiki_Avatar_Fetter_Birth"] = "生日",
["Wiki_Avatar_Fetter_CvChinese"] = "汉语 CV",
["Wiki_Avatar_Fetter_CvJapanese"] = "日语 CV",
["Wiki_Avatar_Fetter_CvEnglish"] = "英语 CV",
["Wiki_Avatar_Fetter_CvKorean"] = "韩语 CV",
["Wiki_Avatar_Subtitle_Skill"] = "天赋",
["Wiki_Avatar_Subtitle_Talent"] = "命之座",
["Wiki_Avatar_Subtitle_Other"] = "其他",
["Wiki_Avatar_Expander_Costumes"] = "衣装",
["Wiki_Avatar_Expander_Fetters"] = "资料",
["Wiki_Avatar_Expander_FetterStories"] = "故事",
["DescParamComboBox_Level"] = "等级",
["GachaLog_Refresh"] = "刷新",
["GachaLog_Refresh_WebCache"] = "从缓存刷新",
["GachaLog_Refresh_ManualInput"] = "手动输入Url",
["GachaLog_Refresh_Aggressive"] = "全量刷新",
["GachaLog_Import"] = "导入",
["GachaLog_Import_UIGFJ"] = "从 UIGF Json 文件导入",
["GachaLog_Import_UIGFW"] = "从 UIGF Excel 文件导入",
["GachaLog_Export"] = "导出",
["GachaLog_Export_UIGFJ"] = "导出到 UIGF Json 文件",
["GachaLog_Export_UIGFW"] = "导出到 UIGF Excel 文件",
["GachaLog_PivotItem_Summary"] = "总览",
["GachaLog_PivotItem_History"] = "历史",
["GachaLog_PivotItem_Avatar"] = "角色",
["GachaLog_PivotItem_Weapon"] = "武器",
["StatisticsCard_Guarantee"] = "保底",
["StatisticsCard_Up"] = "保底",
["StatisticsCard_Pull"] = "抽",
["StatisticsCard_Orange"] = "五星",
["StatisticsCard_Purple"] = "四星",
["StatisticsCard_Blue"] = "三星",
["StatisticsCard_OrangeAverage"] = "五星平均抽数",
["StatisticsCard_UpOrangeAverage"] = "UP 平均抽数",
["Setting_Group_AboutHutao"] = "关于 胡桃",
["Setting_HutaoIcon_Description_Part1"] = "胡桃 图标由 ",
["Setting_HutaoIcon_Description_Part2"] = "纸绘,并由 ",
["Setting_HutaoIcon_Description_Part3"] = " 后期处理后,授权使用。",
["Setting_Feedback_Header"] = "反馈",
["Setting_Feedback_Description"] = "只处理在 Github 上反馈的问题",
["Setting_Feedback_Hyperlink"] = "只处理在 Github 上反馈的问题",
["Setting_UpdateCheck_Header"] = "检查更新",
["Setting_UpdateCheck_Description"] = "根本没有检查更新选项",
["Setting_UpdateCheck_Info"] = "都说了没有了",
["Setting_Group_Experimental"] = "测试功能",
["Setting_DataFolder_Header"] = "打开 数据 文件夹",
["Setting_DataFolder_Description"] = "用户数据/日志/元数据在此处存放",
["Setting_DataFolder_Action"] = "打开",
["Setting_CacheFolder_Header"] = "打开 缓存 文件夹",
["Setting_CacheFolder_Description"] = "图片缓存在此处存放",
["Setting_CacheFolder_Action"] = "打开",
};
/// <inheritdoc/>
public string this[string key]
{
get
{
if (translations.TryGetValue(key, out string? result))
{
return result;
}
return string.Empty;
}
}
}

View File

@@ -2,6 +2,7 @@
x:Class="Snap.Hutao.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shv="using:Snap.Hutao.View"
@@ -13,6 +14,19 @@
Height="44"
Margin="48,0,0,0"/>
<shv:MainView/>
<cwuc:SwitchPresenter x:Name="ContentSwitchPresenter">
<cwuc:Case>
<cwuc:Case.Value>
<x:Boolean>False</x:Boolean>
</cwuc:Case.Value>
<shv:MainView/>
</cwuc:Case>
<cwuc:Case>
<cwuc:Case.Value>
<x:Boolean>True</x:Boolean>
</cwuc:Case.Value>
<shv:WelcomeView/>
</cwuc:Case>
</cwuc:SwitchPresenter>
</Grid>
</Window>

View File

@@ -1,8 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Message;
using Windows.Graphics;
using Windows.Win32.UI.WindowsAndMessaging;
@@ -13,7 +16,7 @@ namespace Snap.Hutao;
/// </summary>
[Injection(InjectAs.Singleton)]
[SuppressMessage("", "CA1001")]
public sealed partial class MainWindow : Window, IExtendedWindowSource
public sealed partial class MainWindow : Window, IExtendedWindowSource, IRecipient<WelcomeStateCompleteMessage>
{
private const int MinWidth = 848;
private const int MinHeight = 524;
@@ -27,6 +30,14 @@ public sealed partial class MainWindow : Window, IExtendedWindowSource
ExtendedWindow<MainWindow>.Initialize(this);
IsPresent = true;
Closed += (s, e) => IsPresent = false;
Ioc.Default.GetRequiredService<IMessenger>().Register(this);
// Query the StaticResourceV1Contract & StaticResourceV2Contract.
// If not complete we should present the welcome view.
ContentSwitchPresenter.Value =
!LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false)
|| (!LocalSetting.Get(SettingKeys.StaticResourceV2Contract, false));
}
/// <summary>
@@ -49,4 +60,10 @@ public sealed partial class MainWindow : Window, IExtendedWindowSource
pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X);
pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y);
}
/// <inheritdoc/>
public void Receive(WelcomeStateCompleteMessage message)
{
ContentSwitchPresenter.Value = false;
}
}

View File

@@ -8,7 +8,6 @@ namespace Snap.Hutao.Message;
/// <summary>
/// 用户切换消息
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal class UserChangedMessage : ValueChangedMessage<User>
{
}

View File

@@ -7,7 +7,6 @@ namespace Snap.Hutao.Message;
/// 值变化消息
/// </summary>
/// <typeparam name="TValue">值的类型</typeparam>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal abstract class ValueChangedMessage<TValue>
where TValue : class
{

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Message;
/// <summary>
/// 欢迎状态完成消息
/// </summary>
public class WelcomeStateCompleteMessage
{
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab;
@@ -48,7 +49,15 @@ public class User : ObservableObject
public UserGameRole? SelectedUserGameRole
{
get => selectedUserGameRole;
set => SetProperty(ref selectedUserGameRole, value);
set
{
if (SetProperty(ref selectedUserGameRole, value))
{
Ioc.Default
.GetRequiredService<IMessenger>()
.Send(new Message.UserChangedMessage() { OldValue = this, NewValue = this });
}
}
}
/// <inheritdoc cref="EntityUser.IsSelected"/>

View File

@@ -41,4 +41,22 @@ public class UserAndRole
{
return new UserAndRole(user.Entity, user.SelectedUserGameRole!);
}
/// <summary>
/// 尝试转换到用户与角色
/// </summary>
/// <param name="user">用户</param>
/// <param name="userAndRole">用户与角色</param>
/// <returns>是否转换成功</returns>
public static bool TryFromUser(User? user, [NotNullWhen(true)]out UserAndRole? userAndRole)
{
if (user != null && user.SelectedUserGameRole != null)
{
userAndRole = FromUser(user);
return true;
}
userAndRole = null;
return false;
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Configuration;
namespace Snap.Hutao.Model.Entity.Database;

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Design;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Context.Database;
@@ -17,7 +16,7 @@ public class AppDbContextDesignTimeFactory : IDesignTimeDbContextFactory<AppDbCo
[EditorBrowsable(EditorBrowsableState.Never)]
public AppDbContext CreateDbContext(string[] args)
{
HutaoContext myDocument = new(new());
return AppDbContext.Create($"Data Source={myDocument.Locate("Userdata.db")}");
string userdataDbName = System.IO.Path.Combine(Core.CoreEnvironment.DataFolder, "Userdata.db");
return AppDbContext.Create($"Data Source={userdataDbName}");
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Design;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Context.Database;
@@ -17,7 +16,7 @@ public class LogDbContextDesignTimeFactory : IDesignTimeDbContextFactory<LogDbCo
[EditorBrowsable(EditorBrowsableState.Never)]
public LogDbContext CreateDbContext(string[] args)
{
HutaoContext myDocument = new(new());
return LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}");
string logDbName = System.IO.Path.Combine(Core.CoreEnvironment.DataFolder, "Log.db");
return LogDbContext.Create($"Data Source={logDbName}");
}
}

View File

@@ -45,6 +45,11 @@ public class SettingEntry
/// </summary>
public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame";
/// <summary>
/// 启动游戏 独占全屏
/// </summary>
public const string LaunchIsExclusive = "Launch.IsExclusive";
/// <summary>
/// 启动游戏 全屏
/// </summary>

View File

@@ -10,8 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class AchievementIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/AchievementIcon/{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
@@ -19,7 +17,7 @@ internal class AchievementIconConverter : ValueConverterBase<string, Uri>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("AchievementIcon", $"{name}.png"));
}
/// <inheritdoc/>

View File

@@ -10,9 +10,8 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class AvatarCardConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/AvatarCard/{0}_Card.png";
private static readonly Uri UIAvatarIconCostumeCard = new("https://static.snapgenshin.com/AvatarCard/UI_AvatarIcon_Costume_Card.png");
private const string CostumeCard = "UI_AvatarIcon_Costume_Card.png";
private static readonly Uri UIAvatarIconCostumeCard = new(Web.HutaoEndpoints.StaticFile("AvatarCard", CostumeCard));
/// <summary>
/// 名称转Uri
@@ -26,7 +25,7 @@ internal class AvatarCardConverter : ValueConverterBase<string, Uri>
return UIAvatarIconCostumeCard;
}
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("AvatarCard", $"{name}_Card.png"));
}
/// <inheritdoc/>

View File

@@ -10,8 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class AvatarIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/AvatarIcon/{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
@@ -19,7 +17,7 @@ internal class AvatarIconConverter : ValueConverterBase<string, Uri>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("AvatarIcon", $"{name}.png"));
}
/// <inheritdoc/>

View File

@@ -10,8 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class AvatarNameCardPicConverter : ValueConverterBase<Avatar.Avatar?, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/NameCardPic/UI_NameCardPic_{0}_P.png";
/// <summary>
/// 从角色转换到名片
/// </summary>
@@ -25,7 +23,7 @@ internal class AvatarNameCardPicConverter : ValueConverterBase<Avatar.Avatar?, U
}
string avatarName = ReplaceSpecialCaseNaming(avatar.Icon["UI_AvatarIcon_".Length..]);
return new Uri(string.Format(BaseUrl, avatarName));
return new Uri(Web.HutaoEndpoints.StaticFile("NameCardPic", $"UI_NameCardPic_{avatarName}_P.png"));
}
/// <inheritdoc/>

View File

@@ -10,8 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class AvatarSideIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/AvatarIcon/{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
@@ -19,7 +17,7 @@ internal class AvatarSideIconConverter : ValueConverterBase<string, Uri>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("AvatarIcon", $"{name}.png"));
}
/// <inheritdoc/>

View File

@@ -12,9 +12,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class ElementNameIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/IconElement/UI_Icon_Element_{0}.png";
private static readonly Uri UIIconNone = new("https://static.snapgenshin.com/Bg/UI_Icon_None.png");
/// <summary>
/// 将中文元素名称转换为图标链接
/// </summary>
@@ -35,8 +32,8 @@ internal class ElementNameIconConverter : ValueConverterBase<string, Uri>
};
return string.IsNullOrEmpty(element)
? UIIconNone
: new Uri(string.Format(BaseUrl, element));
? Web.HutaoEndpoints.UIIconNone
: new Uri(Web.HutaoEndpoints.StaticFile("IconElement", $"UI_Icon_Element_{element}.png"));
}
/// <summary>

View File

@@ -10,8 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class EmotionIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/EmotionIcon/{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
@@ -19,7 +17,7 @@ internal class EmotionIconConverter : ValueConverterBase<string, Uri>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("EmotionIcon", $"{name}.png"));
}
/// <inheritdoc/>

View File

@@ -10,8 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class EquipIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/EquipIcon/{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
@@ -19,7 +17,7 @@ internal class EquipIconConverter : ValueConverterBase<string, Uri>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("EquipIcon", $"{name}.png"));
}
/// <inheritdoc/>
@@ -27,4 +25,4 @@ internal class EquipIconConverter : ValueConverterBase<string, Uri>
{
return IconNameToUri(from);
}
}
}

View File

@@ -10,8 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class GachaAvatarIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/GachaAvatarIcon/UI_Gacha_AvatarIcon_{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
@@ -20,7 +18,7 @@ internal class GachaAvatarIconConverter : ValueConverterBase<string, Uri>
public static Uri IconNameToUri(string name)
{
name = name["UI_AvatarIcon_".Length..];
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("GachaAvatarIcon", $"UI_Gacha_AvatarIcon_{name}.png"));
}
/// <inheritdoc/>

View File

@@ -10,8 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class GachaAvatarImgConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/GachaAvatarImg/UI_Gacha_AvatarImg_{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
@@ -20,7 +18,7 @@ internal class GachaAvatarImgConverter : ValueConverterBase<string, Uri>
public static Uri IconNameToUri(string name)
{
name = name["UI_AvatarIcon_".Length..];
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("GachaAvatarImg", $"UI_Gacha_AvatarImg_{name}.png"));
}
/// <inheritdoc/>

View File

@@ -10,8 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class GachaEquipIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/GachaEquipIcon/UI_Gacha_{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
@@ -20,7 +18,7 @@ internal class GachaEquipIconConverter : ValueConverterBase<string, Uri>
public static Uri IconNameToUri(string name)
{
name = name["UI_".Length..];
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("GachaEquipIcon", $"UI_Gacha_{name}.png"));
}
/// <inheritdoc/>

View File

@@ -10,10 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class ItemIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/ItemIcon/{0}.png";
private static readonly Uri UIItemIconNone = new("https://static.snapgenshin.com/Bg/UI_ItemIcon_None.png");
/// <summary>
/// 名称转Uri
/// </summary>
@@ -21,7 +17,7 @@ internal class ItemIconConverter : ValueConverterBase<string, Uri>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("ItemIcon", $"{name}.png"));
}
/// <inheritdoc/>

View File

@@ -11,8 +11,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class QualityConverter : ValueConverterBase<ItemQuality, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/Bg/UI_{0}.png";
/// <inheritdoc/>
public override Uri Convert(ItemQuality from)
{
@@ -22,6 +20,6 @@ internal class QualityConverter : ValueConverterBase<ItemQuality, Uri>
name = "QUALITY_RED";
}
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("Bg", $"UI_{name}.png"));
}
}

View File

@@ -10,8 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class RelicIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/RelicIcon/{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
@@ -19,7 +17,7 @@ internal class RelicIconConverter : ValueConverterBase<string, Uri>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("RelicIcon", $"{name}.png"));
}
/// <inheritdoc/>

View File

@@ -10,11 +10,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class SkillIconConverter : ValueConverterBase<string, Uri>
{
private const string SkillUrl = "https://static.snapgenshin.com/Skill/{0}.png";
private const string TalentUrl = "https://static.snapgenshin.com/Talent/{0}.png";
private static readonly Uri UIIconNone = new("https://static.snapgenshin.com/Bg/UI_Icon_None.png");
/// <summary>
/// 名称转Uri
/// </summary>
@@ -24,16 +19,16 @@ internal class SkillIconConverter : ValueConverterBase<string, Uri>
{
if (string.IsNullOrWhiteSpace(name))
{
return UIIconNone;
return Web.HutaoEndpoints.UIIconNone;
}
if (name.StartsWith("UI_Talent_"))
{
return new Uri(string.Format(TalentUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("Talent", $"{name}.png"));
}
else
{
return new Uri(string.Format(SkillUrl, name));
return new Uri(Web.HutaoEndpoints.StaticFile("Skill", $"{name}.png"));
}
}

View File

@@ -11,8 +11,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// </summary>
internal class WeaponTypeIconConverter : ValueConverterBase<WeaponType, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/Skill/Skill_A_{0}.png";
/// <summary>
/// 将武器类型转换为图标链接
/// </summary>
@@ -30,7 +28,7 @@ internal class WeaponTypeIconConverter : ValueConverterBase<WeaponType, Uri>
_ => throw Must.NeverHappen(),
};
return new Uri(string.Format(BaseUrl, element));
return new Uri(Web.HutaoEndpoints.StaticFile("Skill", $"Skill_A_{element}.png"));
}
/// <inheritdoc/>

View File

@@ -1,6 +1,5 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": true,
"public": true,
"emitSingleFile": true
"public": true
}

View File

@@ -6,7 +6,6 @@ WM_NCRBUTTONUP
// Type definition
CWMO_FLAGS
HRESULT
MINMAXINFO
// COMCTL32
@@ -30,5 +29,12 @@ CoWaitForMultipleObjects
FindWindowEx
GetDpiForWindow
// COM BITS
BackgroundCopyManager
IBackgroundCopyCallback
IBackgroundCopyFile5
IBackgroundCopyJobHttpOptions
IBackgroundCopyManager
// WinRT
IMemoryBufferByteAccess

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -22,7 +22,6 @@ namespace Snap.Hutao.Service.Achievement;
[Injection(InjectAs.Scoped, typeof(IAchievementService))]
internal class AchievementService : IAchievementService
{
private readonly object saveAchievementLocker = new();
private readonly AppDbContext appDbContext;
private readonly ILogger<AchievementService> logger;
private readonly DbCurrent<EntityArchive, Message.AchievementArchiveChangedMessage> dbCurrent;
@@ -196,7 +195,7 @@ internal class AchievementService : IAchievementService
{
// set to default allow multiple time add
achievement.Entity.InnerId = default;
appDbContext.Achievements.UpdateAndSave(achievement.Entity);
appDbContext.Achievements.AddAndSave(achievement.Entity);
}
else
{

View File

@@ -107,7 +107,7 @@ internal class AvatarInfoService : IAvatarInfoService
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndRole.Role.GameUid);
Summary summary = await GetSummaryCoreAsync(info, GetDbAvatarInfos(userAndRole.Role.GameUid), token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
return new(RefreshResult.Ok, summary);
return new(RefreshResult.Ok, summary.Avatars.Count == 0 ? null : summary);
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 元素精通
/// </summary>
internal static class ElementMastery
{
/// <summary>
/// 增幅反应
/// </summary>
public static readonly ElementMasteryCoefficient ElementAddHurt = new(2.78f, 1400);
/// <summary>
/// 剧变反应
/// </summary>
public static readonly ElementMasteryCoefficient ReactionAddHurt = new(16, 2000);
/// <summary>
/// 激化反应
/// </summary>
public static readonly ElementMasteryCoefficient ReactionOverdoseAddHurt = new(5, 1200);
/// <summary>
/// 激化反应
/// </summary>
public static readonly ElementMasteryCoefficient CrystalShieldHp = new(4.44f, 1400);
/// <summary>
/// 获取差异
/// </summary>
/// <param name="mastery">元素精通</param>
/// <param name="coeff">参数</param>
/// <returns>差异</returns>
public static float GetDelta(float mastery, ElementMasteryCoefficient coeff)
{
return mastery + coeff.P2 == 0 ? 0 : MathF.Max(mastery * coeff.P1 / (mastery + coeff.P2), 0);
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 元素精通系数
/// </summary>
public struct ElementMasteryCoefficient
{
/// <summary>
/// 参数1
/// </summary>
public float P1;
/// <summary>
/// 参数2
/// </summary>
public float P2;
/// <summary>
/// 构造一个新的元素精通系数
/// </summary>
/// <param name="p1">参数1</param>
/// <param name="p2">参数2</param>
public ElementMasteryCoefficient(float p1, float p2)
{
P1 = p1;
P2 = p2;
}
}

View File

@@ -17,8 +17,7 @@ internal class GachaLogUrlManualInputProvider : IGachaLogUrlProvider
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> GetQueryAsync()
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
ValueResult<bool, string> result = await new GachaLogUrlDialog(mainWindow).GetInputUrlAsync().ConfigureAwait(false);
ValueResult<bool, string> result = await new GachaLogUrlDialog().GetInputUrlAsync().ConfigureAwait(false);
if (result.IsOk)
{

View File

@@ -48,7 +48,7 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
{
string cacheFile = GetCacheFile(path);
using (TemporaryFile? tempFile = TemporaryFile.CreateFromFileCopy(cacheFile))
using (TempFile? tempFile = TempFile.CreateFromFileCopy(cacheFile))
{
if (tempFile == null)
{

View File

@@ -65,7 +65,7 @@ internal class GameService : IGameService, IDisposable
{
IEnumerable<IGameLocator> gameLocators = scope.ServiceProvider.GetRequiredService<IEnumerable<IGameLocator>>();
// Try locate by registry
// Try locate by unity log
IGameLocator locator = gameLocators.Single(l => l.Name == nameof(UnityLogGameLocator));
ValueResult<bool, string> result = await locator.LocateGamePathAsync().ConfigureAwait(false);
@@ -304,8 +304,7 @@ internal class GameService : IGameService, IDisposable
if (account == null)
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
(bool isOk, string name) = await new GameAccountNameDialog(mainWindow).GetInputNameAsync().ConfigureAwait(false);
(bool isOk, string name) = await new GameAccountNameDialog().GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
@@ -349,8 +348,7 @@ internal class GameService : IGameService, IDisposable
/// <inheritdoc/>
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
(bool isOk, string name) = await new GameAccountNameDialog(mainWindow).GetInputNameAsync().ConfigureAwait(true);
(bool isOk, string name) = await new GameAccountNameDialog().GetInputNameAsync().ConfigureAwait(true);
if (isOk)
{

View File

@@ -23,7 +23,7 @@ internal partial class UnityLogGameLocator : IGameLocator
string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string logFilePath = Path.Combine(appDataPath, @"..\LocalLow\miHoYo\原神\output_log.txt");
using (TemporaryFile? tempFile = TemporaryFile.CreateFromFileCopy(logFilePath))
using (TempFile? tempFile = TempFile.CreateFromFileCopy(logFilePath))
{
if (tempFile == null)
{

View File

@@ -103,7 +103,7 @@ internal class HutaoService : IHutaoService
appDbContext.ObjectCache.AddAndSave(new()
{
Key = key,
ExpireTime = DateTimeOffset.Now.AddHours(6),
ExpireTime = DateTimeOffset.Now.AddHours(4),
Value = JsonSerializer.Serialize(web, options),
});

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Diagnostics;
@@ -23,12 +22,11 @@ namespace Snap.Hutao.Service.Metadata;
[HttpClient(HttpClientConfigration.Default)]
internal partial class MetadataService : IMetadataService, IMetadataInitializer, ISupportAsyncInitialization
{
private const string MetaAPIHost = "http://hutao-metadata.snapgenshin.com";
private const string MetaFileName = "Meta.json";
private readonly string metadataFolderPath;
private readonly IInfoBarService infoBarService;
private readonly HttpClient httpClient;
private readonly FileSystemContext metadataContext;
private readonly JsonSerializerOptions options;
private readonly ILogger<MetadataService> logger;
private readonly IMemoryCache memoryCache;
@@ -45,24 +43,24 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
/// </summary>
/// <param name="infoBarService">信息条服务</param>
/// <param name="httpClientFactory">http客户端工厂</param>
/// <param name="metadataContext">我的文档上下文</param>
/// <param name="options">json序列化选项</param>
/// <param name="logger">日志器</param>
/// <param name="memoryCache">内存缓存</param>
public MetadataService(
IInfoBarService infoBarService,
IHttpClientFactory httpClientFactory,
MetadataContext metadataContext,
JsonSerializerOptions options,
ILogger<MetadataService> logger,
IMemoryCache memoryCache)
{
this.infoBarService = infoBarService;
this.metadataContext = metadataContext;
this.options = options;
this.logger = logger;
this.memoryCache = memoryCache;
httpClient = httpClientFactory.CreateClient(nameof(MetadataService));
metadataFolderPath = Path.Combine(Core.CoreEnvironment.DataFolder, "Metadata");
Directory.CreateDirectory(metadataFolderPath);
}
/// <inheritdoc/>
@@ -89,12 +87,12 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
private async Task<bool> TryUpdateMetadataAsync(CancellationToken token)
{
IDictionary<string, string>? metaMd5Map = null;
IDictionary<string, string>? metaMd5Map;
try
{
// download meta check file
metaMd5Map = await httpClient
.GetFromJsonAsync<IDictionary<string, string>>($"{MetaAPIHost}/{MetaFileName}", options, token)
.GetFromJsonAsync<IDictionary<string, string>>(Web.HutaoEndpoints.HutaoMetadataFile(MetaFileName), options, token)
.ConfigureAwait(false);
if (metaMd5Map is null)
@@ -112,7 +110,7 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
await CheckMetadataAsync(metaMd5Map, token).ConfigureAwait(false);
// save metadataFile
using (FileStream metaFileStream = metadataContext.Create(MetaFileName))
using (FileStream metaFileStream = File.Create(Path.Combine(metadataFolderPath, MetaFileName)))
{
await JsonSerializer
.SerializeAsync(metaFileStream, metaMd5Map, options, token)
@@ -137,7 +135,7 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
string fileFullName = $"{fileName}.json";
bool skip = false;
if (metadataContext.FileExists(fileFullName))
if (File.Exists(Path.Combine(metadataFolderPath, fileFullName)))
{
skip = md5 == await GetFileMd5Async(fileFullName, token).ConfigureAwait(false);
}
@@ -153,7 +151,7 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
private async Task<string> GetFileMd5Async(string fileFullName, CancellationToken token)
{
using (FileStream stream = metadataContext.OpenRead(fileFullName))
using (FileStream stream = File.OpenRead(Path.Combine(metadataFolderPath, fileFullName)))
{
byte[] bytes = await MD5.Create()
.ComputeHashAsync(stream, token)
@@ -166,13 +164,13 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
private async Task DownloadMetadataAsync(string fileFullName, CancellationToken token)
{
Stream sourceStream = await httpClient
.GetStreamAsync($"{MetaAPIHost}/{fileFullName}", token)
.GetStreamAsync(Web.HutaoEndpoints.HutaoMetadataFile(fileFullName), token)
.ConfigureAwait(false);
// Write stream while convert LF to CRLF
using (StreamReader streamReader = new(sourceStream))
{
using (StreamWriter streamWriter = new(metadataContext.Create(fileFullName)))
using (StreamWriter streamWriter = new(File.Create(Path.Combine(metadataFolderPath, fileFullName))))
{
while (await streamReader.ReadLineAsync(token).ConfigureAwait(false) is string line)
{
@@ -196,7 +194,7 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
return Must.NotNull((T)value!);
}
using (Stream fileStream = metadataContext.OpenRead($"{fileName}.json"))
using (Stream fileStream = File.OpenRead(Path.Combine(metadataFolderPath, $"{fileName}.json")))
{
T? result = await JsonSerializer.DeserializeAsync<T>(fileStream, options, token).ConfigureAwait(false);
return memoryCache.Set(cacheKey, Must.NotNull(result!));

View File

@@ -15,8 +15,9 @@ internal interface ISpiralAbyssRecordService
/// <summary>
/// 异步获取深渊记录集合
/// </summary>
/// <param name="userAndRole">当前角色</param>
/// <returns>深渊记录集合</returns>
Task<ObservableCollection<SpiralAbyssEntry>> GetSpiralAbyssCollectionAsync();
Task<ObservableCollection<SpiralAbyssEntry>> GetSpiralAbyssCollectionAsync(UserAndRole userAndRole);
/// <summary>
/// 异步刷新深渊记录

View File

@@ -20,6 +20,7 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
private readonly AppDbContext appDbContext;
private readonly GameRecordClient gameRecordClient;
private string? uid;
private ObservableCollection<SpiralAbyssEntry>? spiralAbysses;
/// <summary>
@@ -34,12 +35,19 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
}
/// <inheritdoc/>
public async Task<ObservableCollection<SpiralAbyssEntry>> GetSpiralAbyssCollectionAsync()
public async Task<ObservableCollection<SpiralAbyssEntry>> GetSpiralAbyssCollectionAsync(UserAndRole userAndRole)
{
if (uid != userAndRole.Role.GameUid)
{
spiralAbysses = null;
}
uid = userAndRole.Role.GameUid;
if (spiralAbysses == null)
{
List<SpiralAbyssEntry> entries = await appDbContext.SpiralAbysses
.AsNoTracking()
.Where(s => s.Uid == userAndRole.Role.GameUid)
.OrderByDescending(s => s.ScheduleId)
.ToListAsync()
.ConfigureAwait(false);
@@ -73,10 +81,11 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
{
SpiralAbyssEntry entry = SpiralAbyssEntry.Create(userAndRole.Role.GameUid, last);
await appDbContext.SpiralAbysses.AddAndSaveAsync(entry).ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
spiralAbysses!.Insert(0, entry);
await ThreadHelper.SwitchToBackgroundAsync();
await appDbContext.SpiralAbysses.AddAndSaveAsync(entry).ConfigureAwait(false);
}
}
@@ -99,10 +108,11 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
{
SpiralAbyssEntry entry = SpiralAbyssEntry.Create(userAndRole.Role.GameUid, current);
await appDbContext.SpiralAbysses.AddAndSaveAsync(entry).ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
spiralAbysses!.Insert(0, entry);
await ThreadHelper.SwitchToBackgroundAsync();
await appDbContext.SpiralAbysses.AddAndSaveAsync(entry).ConfigureAwait(false);
}
}
}

View File

@@ -64,16 +64,17 @@
<None Remove="Resource\Icon\UI_MarkTower.png" />
<None Remove="Resource\Icon\UI_MarkTower_Tower.png" />
<None Remove="Resource\Segoe Fluent Icons.ttf" />
<None Remove="Resource\WelcomeView_Background.png" />
<None Remove="stylecop.json" />
<None Remove="View\Control\BottomTextControl.xaml" />
<None Remove="View\Control\DescParamComboBox.xaml" />
<None Remove="View\Control\ItemIcon.xaml" />
<None Remove="View\Control\LoadingView.xaml" />
<None Remove="View\Control\SkillPivot.xaml" />
<None Remove="View\Control\StatisticsCard.xaml" />
<None Remove="View\Dialog\AchievementArchiveCreateDialog.xaml" />
<None Remove="View\Dialog\AchievementImportDialog.xaml" />
<None Remove="View\Dialog\AdoptCalculatorDialog.xaml" />
<None Remove="View\Dialog\AvatarInfoQueryDialog.xaml" />
<None Remove="View\Dialog\CommunityGameRecordDialog.xaml" />
<None Remove="View\Dialog\CultivateProjectDialog.xaml" />
<None Remove="View\Dialog\CultivatePromotionDeltaDialog.xaml" />
@@ -99,10 +100,12 @@
<None Remove="View\Page\LoginMihoyoUserPage.xaml" />
<None Remove="View\Page\SettingPage.xaml" />
<None Remove="View\Page\SpiralAbyssRecordPage.xaml" />
<None Remove="View\Page\TestPage.xaml" />
<None Remove="View\Page\WikiAvatarPage.xaml" />
<None Remove="View\Page\WikiWeaponPage.xaml" />
<None Remove="View\TitleView.xaml" />
<None Remove="View\UserView.xaml" />
<None Remove="View\WelcomeView.xaml" />
</ItemGroup>
<ItemGroup>
@@ -143,6 +146,7 @@
<Content Include="Resource\Icon\UI_MarkQuest_Events_Proce.png" />
<Content Include="Resource\Icon\UI_MarkTower.png" />
<Content Include="Resource\Icon\UI_MarkTower_Tower.png" />
<Content Include="Resource\WelcomeView_Background.png" />
</ItemGroup>
<ItemGroup>
@@ -158,11 +162,11 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.4.27">
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.5.10-alpha">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.64" />
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.65" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.138-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -191,6 +195,21 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Page Update="View\WelcomeView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\TestPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Control\LoadingView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\SpiralAbyssRecordPage.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -286,11 +305,6 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\AvatarInfoQueryDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\AvatarPropertyPage.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -14,8 +14,9 @@
</UserControl.Resources>
<Grid>
<Grid CornerRadius="{StaticResource CompatCornerRadius}">
<shci:CachedImage Source="{x:Bind Quality, Converter={StaticResource QualityConverter}, Mode=OneWay}"/>
<shci:CachedImage Source="https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png"/>
<!-- Disable some CachedImage's LazyLoading function here can increase response speed -->
<shci:CachedImage EnableLazyLoading="False" Source="{x:Bind Quality, Converter={StaticResource QualityConverter}, Mode=OneWay}"/>
<shci:CachedImage EnableLazyLoading="False" Source="{StaticResource UI_ImgSign_ItemIcon}"/>
<shci:CachedImage Source="{x:Bind Icon, Mode=OneWay}"/>
<shci:CachedImage
Width="16"
@@ -23,6 +24,7 @@
Margin="2"
HorizontalAlignment="Left"
VerticalAlignment="Top"
EnableLazyLoading="False"
Source="{x:Bind Badge, Mode=OneWay}"/>
</Grid>
</Grid>

View File

@@ -0,0 +1,23 @@
<cwuc:Loading
x:Class="Snap.Hutao.View.Control.LoadingView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shci="using:Snap.Hutao.Control.Image"
mc:Ignorable="d">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<shci:CachedImage
Width="120"
Height="120"
Source="{StaticResource UI_EmotionIcon272}"/>
<TextBlock
Margin="0,16,0,0"
HorizontalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="加载中,请稍候"/>
<ProgressRing Margin="0,16,0,0" IsActive="True"/>
</StackPanel>
</cwuc:Loading>

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Controls;
namespace Snap.Hutao.View.Control;
/// <summary>
/// 加载视图
/// </summary>
public sealed partial class LoadingView : Loading
{
/// <summary>
/// 构造一个新的加载视图
/// </summary>
public LoadingView()
{
InitializeComponent();
}
}

View File

@@ -52,11 +52,12 @@
Visibility="{Binding IsUp, Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock
Width="20"
Width="24"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding LastPull}"
TextAlignment="Center"/>
TextAlignment="Center"
TextWrapping="NoWrap"/>
</StackPanel>
</Grid>
</DataTemplate>
@@ -91,7 +92,6 @@
</Border>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Border Background="{StaticResource CardBackgroundFillColorDefaultBrush}" CornerRadius="{StaticResource CompatCornerRadius}">
@@ -123,7 +123,10 @@
FontSize="24"
Text="{Binding TotalCount}"
Visibility="{Binding ElementName=DetailExpander, Path=IsExpanded, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<shcp:PanelSelector x:Name="ItemsPanelSelector" Margin="6,0,6,0"/>
<shcp:PanelSelector
x:Name="ItemsPanelSelector"
Margin="6,0"
Current="Grid"/>
</StackPanel>
</Grid>
</Expander.Header>

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Converters;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.View.Converter;

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Converters;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.View.Converter;

View File

@@ -19,4 +19,4 @@ public class EmptyCollectionToVisibilityConverter : EmptyCollectionToObjectConve
EmptyValue = Visibility.Collapsed;
NotEmptyValue = Visibility.Visible;
}
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Converters;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.View.Converter;

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Converters;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.View.Converter;

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.View.Converter;
/// <summary>
/// Int32 转 Visibility
/// </summary>
public class Int32ToVisibilityConverter : IValueConverter
{
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
return (int)value == 0 ? Visibility.Collapsed : Visibility.Visible;
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.View.Converter;
/// <summary>
/// Int32 反转 Visibility
/// </summary>
public class Int32ToVisibilityRevertConverter : IValueConverter
{
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
return (int)value == 0 ? Visibility.Visible : Visibility.Collapsed;
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -15,10 +15,10 @@ public sealed partial class AchievementArchiveCreateDialog : ContentDialog
/// 构造一个新的成就存档创建对话框
/// </summary>
/// <param name="window">窗体</param>
public AchievementArchiveCreateDialog(Window window)
public AchievementArchiveCreateDialog()
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
XamlRoot = Ioc.Default.GetRequiredService<MainWindow>().Content.XamlRoot;
}
/// <summary>
@@ -27,6 +27,7 @@ public sealed partial class AchievementArchiveCreateDialog : ContentDialog
/// <returns>输入的结果</returns>
public async Task<ValueResult<bool, string>> GetInputAsync()
{
await ThreadHelper.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();
string text = InputText.Text ?? string.Empty;

View File

@@ -19,12 +19,11 @@ public sealed partial class AchievementImportDialog : ContentDialog
/// <summary>
/// 构造一个新的成就对话框
/// </summary>
/// <param name="window">呈现的父窗口</param>
/// <param name="uiaf">uiaf数据</param>
public AchievementImportDialog(Window window, UIAF uiaf)
public AchievementImportDialog(UIAF uiaf)
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
XamlRoot = Ioc.Default.GetRequiredService<MainWindow>().Content.XamlRoot;
UIAF = uiaf;
}
@@ -43,6 +42,7 @@ public sealed partial class AchievementImportDialog : ContentDialog
/// <returns>导入选项</returns>
public async Task<ValueResult<bool, ImportStrategy>> GetImportStrategyAsync()
{
await ThreadHelper.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();
ImportStrategy strategy = (ImportStrategy)ImportModeSelector.SelectedIndex;

View File

@@ -24,11 +24,11 @@ public sealed partial class AdoptCalculatorDialog : ContentDialog
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µ<EFBFBD><C2B5><EFBFBD><EFBFBD>ɼ<EFBFBD><C9BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
/// <param name="window"><3E><><EFBFBD><EFBFBD></param>
public AdoptCalculatorDialog(Window window)
public AdoptCalculatorDialog()
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
scope = Ioc.Default.CreateScope();
XamlRoot = scope.ServiceProvider.GetRequiredService<MainWindow>().Content.XamlRoot;
}
private void OnGridLoaded(object sender, RoutedEventArgs e)

View File

@@ -1,21 +0,0 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.AvatarInfoQueryDialog"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="查询UID对应的橱窗"
CloseButtonText="取消"
DefaultButton="Primary"
IsPrimaryButtonEnabled="False"
PrimaryButtonText="请输入UID"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<Grid>
<TextBox
x:Name="InputText"
PlaceholderText="请输入UID"
TextChanged="InputTextChanged"/>
</Grid>
</ContentDialog>

View File

@@ -1,53 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.View.Dialog;
/// <summary>
/// 角色信息查询UID对话框
/// </summary>
public sealed partial class AvatarInfoQueryDialog : ContentDialog
{
/// <summary>
/// 构造一个新的角色信息查询UID对话框
/// </summary>
/// <param name="window">窗口</param>
public AvatarInfoQueryDialog(Window window)
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
}
/// <summary>
/// 获取玩家UID
/// </summary>
/// <returns>玩家UID</returns>
public async Task<ValueResult<bool, PlayerUid>> GetPlayerUidAsync()
{
ContentDialogResult result = await ShowAsync();
bool isOk = result == ContentDialogResult.Primary;
if (InputText.Text.Length != 9)
{
return new(false, default);
}
return new(isOk, isOk && InputText.Text.Length == 9 ? new(InputText.Text) : default);
}
private void InputTextChanged(object sender, TextChangedEventArgs e)
{
bool inputValid = string.IsNullOrEmpty(InputText.Text) && InputText.Text.Length == 9;
(PrimaryButtonText, IsPrimaryButtonEnabled) = inputValid switch
{
true => ("请输入正确的UID", false),
false => ("确认", true),
};
}
}

View File

@@ -24,11 +24,11 @@ public sealed partial class CommunityGameRecordDialog : ContentDialog
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µ<EFBFBD><C2B5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϸ<EFBFBD><CFB7>¼<EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
/// <param name="window"><3E><><EFBFBD><EFBFBD></param>
public CommunityGameRecordDialog(MainWindow window)
public CommunityGameRecordDialog()
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
scope = Ioc.Default.CreateScope();
XamlRoot = scope.ServiceProvider.GetRequiredService<MainWindow>().Content.XamlRoot;
}
private void OnGridLoaded(object sender, RoutedEventArgs e)

View File

@@ -17,10 +17,10 @@ public sealed partial class CultivateProjectDialog : ContentDialog
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µ<EFBFBD><C2B5><EFBFBD><EFBFBD>ɼƻ<C9BC><C6BB>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
/// <param name="window"><3E><><EFBFBD><EFBFBD></param>
public CultivateProjectDialog(Window window)
public CultivateProjectDialog()
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
XamlRoot = Ioc.Default.GetRequiredService<MainWindow>().Content.XamlRoot;
}
/// <summary>

View File

@@ -19,13 +19,12 @@ public sealed partial class CultivatePromotionDeltaDialog : ContentDialog
/// <summary>
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µ<EFBFBD><C2B5><EFBFBD><EFBFBD>ɼ<EFBFBD><C9BC><EFBFBD><EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
/// <param name="window"><3E><><EFBFBD><EFBFBD></param>
/// <param name="avatar"><3E><>ɫ</param>
/// <param name="weapon"><3E><><EFBFBD><EFBFBD></param>
public CultivatePromotionDeltaDialog(Window window, ICalculableAvatar? avatar, ICalculableWeapon? weapon)
public CultivatePromotionDeltaDialog(ICalculableAvatar? avatar, ICalculableWeapon? weapon)
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
XamlRoot = Ioc.Default.GetRequiredService<MainWindow>().Content.XamlRoot;
DataContext = this;
Avatar = avatar;
Weapon = weapon;
@@ -55,6 +54,7 @@ public sealed partial class CultivatePromotionDeltaDialog : ContentDialog
/// <returns><3E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD></returns>
public async Task<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaAsync()
{
await ThreadHelper.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();
if (result == ContentDialogResult.Primary)

Some files were not shown because too many files have changed in this diff Show More