mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23f3e5df77 | ||
|
|
4a027a8d3f | ||
|
|
80459708a7 | ||
|
|
650b67bea0 | ||
|
|
18b3d23b1c |
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
50
src/Snap.Hutao/Snap.Hutao/Control/Image/GradientDirection.cs
Normal file
50
src/Snap.Hutao/Snap.Hutao/Control/Image/GradientDirection.cs
Normal 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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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=""/>
|
||||
</SplitButton.Content>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
311
src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsJob.cs
Normal file
311
src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsJob.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
77
src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsManager.cs
Normal file
77
src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
67
src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs
Normal file
67
src/Snap.Hutao/Snap.Hutao/Factory/ContentDialogFactory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ namespace Snap.Hutao.Message;
|
||||
/// <summary>
|
||||
/// 用户切换消息
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
internal class UserChangedMessage : ValueChangedMessage<User>
|
||||
{
|
||||
}
|
||||
@@ -7,7 +7,6 @@ namespace Snap.Hutao.Message;
|
||||
/// 值变化消息
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">值的类型</typeparam>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
internal abstract class ValueChangedMessage<TValue>
|
||||
where TValue : class
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ public class SettingEntry
|
||||
/// </summary>
|
||||
public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 独占全屏
|
||||
/// </summary>
|
||||
public const string LaunchIsExclusive = "Launch.IsExclusive";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 全屏
|
||||
/// </summary>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"allowMarshaling": true,
|
||||
"public": true,
|
||||
"emitSingleFile": true
|
||||
"public": true
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
BIN
src/Snap.Hutao/Snap.Hutao/Resource/WelcomeView_Background.png
Normal file
BIN
src/Snap.Hutao/Snap.Hutao/Resource/WelcomeView_Background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
@@ -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!));
|
||||
|
||||
@@ -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>
|
||||
/// 异步刷新深渊记录
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
23
src/Snap.Hutao/Snap.Hutao/View/Control/LoadingView.xaml
Normal file
23
src/Snap.Hutao/Snap.Hutao/View/Control/LoadingView.xaml
Normal 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>
|
||||
20
src/Snap.Hutao/Snap.Hutao/View/Control/LoadingView.xaml.cs
Normal file
20
src/Snap.Hutao/Snap.Hutao/View/Control/LoadingView.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Converters;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.View.Converter;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Converters;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.View.Converter;
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ public class EmptyCollectionToVisibilityConverter : EmptyCollectionToObjectConve
|
||||
EmptyValue = Visibility.Collapsed;
|
||||
NotEmptyValue = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Converters;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.View.Converter;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Converters;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.View.Converter;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user