mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ public class CachedImage : ImageEx
|
||||
{
|
||||
IsCacheEnabled = true;
|
||||
EnableLazyLoading = true;
|
||||
LazyLoadingThreshold = 500;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -33,18 +34,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,6 +9,7 @@ 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;
|
||||
@@ -60,15 +61,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 +132,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 +140,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,7 +3,9 @@
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Storage;
|
||||
@@ -16,8 +18,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 { return (GradientDirection)GetValue(BackgroundDirectionProperty); }
|
||||
set { SetValue(BackgroundDirectionProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 前景方向
|
||||
/// </summary>
|
||||
public GradientDirection ForegroundDirection
|
||||
{
|
||||
get { return (GradientDirection)GetValue(ForegroundDirectionProperty); }
|
||||
set { SetValue(ForegroundDirectionProperty, value); }
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnUpdateVisual(SpriteVisual spriteVisual)
|
||||
{
|
||||
@@ -29,15 +52,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 +75,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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,23 +12,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!;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
82
src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsManager.cs
Normal file
82
src/Snap.Hutao/Snap.Hutao/Core/IO/Bits/BitsManager.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Core.IO;
|
||||
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 管理器
|
||||
/// </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;
|
||||
}
|
||||
|
||||
@@ -17,4 +17,14 @@ 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";
|
||||
}
|
||||
@@ -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,12 @@ 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.
|
||||
// If not complete we should present the welcome view.
|
||||
ContentSwitchPresenter.Value = !LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,4 +58,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"/>
|
||||
|
||||
@@ -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,50 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Annotation;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Converter;
|
||||
using ModelPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ 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 IInfoBarService infoBarService;
|
||||
@@ -89,12 +88,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)
|
||||
@@ -166,7 +165,7 @@ 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
|
||||
|
||||
@@ -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,10 +64,12 @@
|
||||
<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" />
|
||||
@@ -99,10 +101,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 +147,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>
|
||||
@@ -191,6 +196,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -19,4 +19,4 @@ public class EmptyCollectionToVisibilityConverter : EmptyCollectionToObjectConve
|
||||
EmptyValue = Visibility.Collapsed;
|
||||
NotEmptyValue = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Converters;
|
||||
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,26 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Converters;
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
Margin="12,0,0,0"
|
||||
Padding="6"
|
||||
Content="前往下载"
|
||||
NavigateUri="https://github.com/HolographicHat/GetToken/releases/latest"/>
|
||||
NavigateUri="{StaticResource HolographicHat_GetToken_Release}"/>
|
||||
</wsc:Setting>
|
||||
<wsc:Setting
|
||||
HorizontalAlignment="Stretch"
|
||||
@@ -41,7 +41,7 @@
|
||||
Margin="12,0,0,0"
|
||||
Padding="6"
|
||||
Content="立即前往"
|
||||
NavigateUri="https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie"/>
|
||||
NavigateUri="{StaticResource DocumentLink_MhyAccountSwitch}"/>
|
||||
</wsc:Setting>
|
||||
</wsc:SettingsGroup>
|
||||
</StackPanel>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Background="{StaticResource CardBackgroundFillColorDefaultBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="252"/>
|
||||
<ColumnDefinition Width="{StaticResource CompatGridLength2}"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
Grid.Row="1"
|
||||
DisplayMode="Inline"
|
||||
IsPaneOpen="True"
|
||||
OpenPaneLength="252"
|
||||
OpenPaneLength="{StaticResource CompatSplitViewOpenPaneLength2}"
|
||||
PaneBackground="Transparent">
|
||||
<SplitView.Pane>
|
||||
<ListView
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
Grid.Row="1"
|
||||
DisplayMode="Inline"
|
||||
IsPaneOpen="True"
|
||||
OpenPaneLength="200"
|
||||
OpenPaneLength="{StaticResource CompatSplitViewOpenPaneLength}"
|
||||
PaneBackground="Transparent">
|
||||
<SplitView.Pane>
|
||||
<cwucont:SwitchPresenter Value="{Binding ElementName=ItemsPanelSelector, Path=Current}">
|
||||
@@ -117,17 +117,17 @@
|
||||
<cwucont:Case Value="Grid">
|
||||
<GridView
|
||||
Margin="6,6,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding Summary.Avatars}"
|
||||
SelectedItem="{Binding SelectedAvatar, Mode=TwoWay}"
|
||||
SelectionMode="Single">
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="40"
|
||||
Height="40"
|
||||
Margin="0"
|
||||
Source="{Binding Icon, Mode=OneWay}"/>
|
||||
<shvcont:ItemIcon
|
||||
Width="44"
|
||||
Height="44"
|
||||
Icon="{Binding Icon}"
|
||||
Quality="{Binding Quality}"/>
|
||||
</DataTemplate>
|
||||
</GridView.ItemTemplate>
|
||||
</GridView>
|
||||
@@ -567,7 +567,7 @@
|
||||
<shci:CachedImage
|
||||
Width="120"
|
||||
Height="120"
|
||||
Source="https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon250.png"/>
|
||||
Source="{StaticResource UI_EmotionIcon250}"/>
|
||||
<TextBlock
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
<Grid>
|
||||
<Grid Visibility="{Binding Projects, Converter={StaticResource EmptyCollectionToVisibilityConverter}}">
|
||||
<Grid Visibility="{Binding Projects.Count, Converter={StaticResource Int32ToVisibilityConverter}}">
|
||||
<Rectangle
|
||||
Height="48"
|
||||
VerticalAlignment="Top"
|
||||
@@ -351,12 +351,12 @@
|
||||
</Pivot>
|
||||
</Grid>
|
||||
|
||||
<Grid Visibility="{Binding Projects, Converter={StaticResource EmptyCollectionToVisibilityRevertConverter}}">
|
||||
<Grid Visibility="{Binding Projects.Count, Converter={StaticResource Int32ToVisibilityRevertConverter}}">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<shci:CachedImage
|
||||
Width="120"
|
||||
Height="120"
|
||||
Source="https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon293.png"/>
|
||||
Source="{StaticResource UI_EmotionIcon293}"/>
|
||||
<TextBlock
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
@@ -234,7 +234,8 @@
|
||||
<TextBlock
|
||||
Margin="0,16,0,8"
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="五星"/>
|
||||
Text="五星"
|
||||
Visibility="{Binding SelectedHistoryWish.OrangeList, Converter={StaticResource EmptyCollectionToVisibilityConverter}}"/>
|
||||
<GridView ItemsSource="{Binding SelectedHistoryWish.OrangeList}" SelectionMode="None">
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
@@ -261,7 +262,8 @@
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="四星"/>
|
||||
Text="四星"
|
||||
Visibility="{Binding SelectedHistoryWish.PurpleList, Converter={StaticResource EmptyCollectionToVisibilityConverter}}"/>
|
||||
<GridView ItemsSource="{Binding SelectedHistoryWish.PurpleList}" SelectionMode="None">
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
@@ -288,7 +290,8 @@
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="三星"/>
|
||||
Text="三星"
|
||||
Visibility="{Binding SelectedHistoryWish.BlueList, Converter={StaticResource EmptyCollectionToVisibilityConverter}}"/>
|
||||
<GridView ItemsSource="{Binding SelectedHistoryWish.BlueList}" SelectionMode="None">
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
@@ -468,7 +471,7 @@
|
||||
<shci:CachedImage
|
||||
Width="120"
|
||||
Height="120"
|
||||
Source="https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon71.png"/>
|
||||
Source="{StaticResource UI_EmotionIcon71}"/>
|
||||
<TextBlock
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
@@ -273,18 +273,11 @@
|
||||
<ItemsControl
|
||||
Margin="0,0,0,8"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ItemsPanel="{StaticResource ItemsStackPanelTemplate}"
|
||||
ItemsSource="{Binding AvatarConstellationInfos}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<ItemsStackPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
Margin="16,0,16,8"
|
||||
Background="{StaticResource CardBackgroundFillColorDefault}"
|
||||
CornerRadius="{StaticResource CompatCornerRadius}">
|
||||
<Border Margin="16,0,16,8" Style="{StaticResource BorderCardStyle}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
@@ -331,19 +324,6 @@
|
||||
</Grid>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
<cwuc:Loading IsLoading="{Binding Overview, Converter={StaticResource EmptyObjectToBoolRevertConverter}}">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<shci:CachedImage
|
||||
Width="120"
|
||||
Height="120"
|
||||
Source="https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png"/>
|
||||
<TextBlock
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="加载中,请稍候"/>
|
||||
<ProgressRing Margin="0,16,0,0" IsActive="True"/>
|
||||
</StackPanel>
|
||||
</cwuc:Loading>
|
||||
<shvc:LoadingView IsLoading="{Binding Overview, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
|
||||
</Grid>
|
||||
</shc:ScopedPage>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
Description="Github 上反馈的问题会优先处理"
|
||||
Header="反馈"
|
||||
Icon="">
|
||||
<HyperlinkButton Content="前往反馈" NavigateUri="https://hut.ao/statements/bug-report.html"/>
|
||||
<HyperlinkButton Content="前往反馈" NavigateUri="{StaticResource DocumentLink_BugReport}"/>
|
||||
</wsc:Setting>
|
||||
<wsc:SettingExpander>
|
||||
<wsc:SettingExpander.Header>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
IsPaneOpen="True"
|
||||
OpenPaneLength="96"
|
||||
PaneBackground="Transparent"
|
||||
Visibility="{Binding SpiralAbyssEntries, Converter={StaticResource EmptyCollectionToVisibilityConverter}}">
|
||||
Visibility="{Binding SpiralAbyssView, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
|
||||
<SplitView.Pane>
|
||||
<ListView ItemsSource="{Binding SpiralAbyssEntries}" SelectedItem="{Binding SelectedEntry, Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
@@ -158,17 +158,20 @@
|
||||
</ScrollViewer>
|
||||
</PivotItem>
|
||||
<PivotItem DataContext="{Binding SpiralAbyssView}" Header="详细数据">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto">
|
||||
<ScrollViewer VerticalAlignment="Top" HorizontalScrollBarVisibility="Auto">
|
||||
<ItemsControl
|
||||
Margin="16,16,0,0"
|
||||
ItemsPanel="{StaticResource HorizontalStackPanelTemplate}"
|
||||
ItemsSource="{Binding Floors}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Margin="0,0,16,16" Style="{StaticResource BorderCardStyle}">
|
||||
<Grid VerticalAlignment="Top">
|
||||
<Border
|
||||
Margin="0,0,16,16"
|
||||
VerticalAlignment="Top"
|
||||
Style="{StaticResource BorderCardStyle}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0" Margin="8,8,8,0">
|
||||
@@ -281,12 +284,12 @@
|
||||
</Grid>
|
||||
</SplitView.Content>
|
||||
</SplitView>
|
||||
<Grid Visibility="{Binding SpiralAbyssEntries, Converter={StaticResource EmptyCollectionToVisibilityRevertConverter}}">
|
||||
<Grid Visibility="{Binding SpiralAbyssView, Converter={StaticResource EmptyObjectToVisibilityRevertConverter}}">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<shci:CachedImage
|
||||
Width="120"
|
||||
Height="120"
|
||||
Source="https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon25.png"/>
|
||||
Source="{StaticResource UI_EmotionIcon25}"/>
|
||||
<TextBlock
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
33
src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml
Normal file
33
src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml
Normal file
@@ -0,0 +1,33 @@
|
||||
<shc:ScopedPage
|
||||
x:Class="Snap.Hutao.View.Page.TestPage"
|
||||
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"
|
||||
xmlns:shc="using:Snap.Hutao.Control"
|
||||
xmlns:shv="using:Snap.Hutao.ViewModel"
|
||||
xmlns:wsc="using:WinUICommunity.SettingsUI.Controls"
|
||||
d:DataContext="{d:DesignInstance shv:TestViewModel}"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<Style BasedOn="{StaticResource SettingButtonStyle}" TargetType="Button">
|
||||
<Setter Property="MinWidth" Value="160"/>
|
||||
</Style>
|
||||
</Page.Resources>
|
||||
<ScrollViewer>
|
||||
<wsc:SettingsGroup Margin="16,0,16,16" Header="This page is only for the test purpose.">
|
||||
<wsc:Setting Header="DangerousLoginMihoyoBbsTest">
|
||||
<Button Command="{Binding DangerousLoginMihoyoBbsCommand}" Content="Login"/>
|
||||
</wsc:Setting>
|
||||
|
||||
<wsc:Setting Header="CommunityGameRecordDialogTest">
|
||||
<Button Command="{Binding ShowCommunityGameRecordDialogCommand}" Content="Open"/>
|
||||
</wsc:Setting>
|
||||
|
||||
<wsc:Setting Header="DownloadStaticFileTest">
|
||||
<Button Command="{Binding DownloadStaticFileCommand}" Content="Download"/>
|
||||
</wsc:Setting>
|
||||
</wsc:SettingsGroup>
|
||||
</ScrollViewer>
|
||||
</shc:ScopedPage>
|
||||
22
src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml.cs
Normal file
22
src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.ViewModel;
|
||||
|
||||
namespace Snap.Hutao.View.Page;
|
||||
|
||||
/// <summary>
|
||||
/// <20><><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3>
|
||||
/// </summary>
|
||||
public sealed partial class TestPage : ScopedPage
|
||||
{
|
||||
/// <summary>
|
||||
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µIJ<C2B5><C4B2><EFBFBD>ҳ<EFBFBD><D2B3>
|
||||
/// </summary>
|
||||
public TestPage()
|
||||
{
|
||||
InitializeWith<TestViewModel>();
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@
|
||||
Grid.Row="1"
|
||||
DisplayMode="Inline"
|
||||
IsPaneOpen="True"
|
||||
OpenPaneLength="200"
|
||||
OpenPaneLength="{StaticResource CompatSplitViewOpenPaneLength}"
|
||||
PaneBackground="{StaticResource CardBackgroundFillColorSecondaryBrush}">
|
||||
<SplitView.Pane>
|
||||
<cwuc:SwitchPresenter Grid.Row="1" Value="{Binding ElementName=ItemsPanelSelector, Path=Current}">
|
||||
@@ -136,19 +136,18 @@
|
||||
<cwuc:Case Value="Grid">
|
||||
<GridView
|
||||
Margin="6,6,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
ItemsSource="{Binding Avatars}"
|
||||
SelectedItem="{Binding Selected, Mode=TwoWay}"
|
||||
SelectionMode="Single">
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="40"
|
||||
Height="40"
|
||||
Margin="0"
|
||||
Source="{Binding Icon, Converter={StaticResource AvatarIconConverter}, Mode=OneWay}"/>
|
||||
<shvc:ItemIcon
|
||||
Width="44"
|
||||
Height="44"
|
||||
Icon="{Binding Icon, Converter={StaticResource AvatarIconConverter}, Mode=OneWay}"
|
||||
Quality="{Binding Quality}"/>
|
||||
</DataTemplate>
|
||||
</GridView.ItemTemplate>
|
||||
</GridView>
|
||||
@@ -158,7 +157,11 @@
|
||||
<SplitView.Content>
|
||||
<Grid>
|
||||
<!-- 渐变背景 -->
|
||||
<shci:Gradient VerticalAlignment="Top" Source="{Binding Selected, Converter={StaticResource AvatarNameCardPicConverter}}"/>
|
||||
<shci:Gradient
|
||||
VerticalAlignment="Top"
|
||||
BackgroundDirection="RightTopToLeftBottom"
|
||||
ForegroundDirection="TopToBottom"
|
||||
Source="{Binding Selected, Converter={StaticResource AvatarNameCardPicConverter}}"/>
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel
|
||||
@@ -385,7 +388,7 @@
|
||||
<shvc:ItemIcon
|
||||
Width="80"
|
||||
Height="80"
|
||||
Icon="https://static.snapgenshin.com/Bg/UI_ItemIcon_None.png"
|
||||
Icon="{StaticResource UI_ItemIcon_None}"
|
||||
Quality="QUALITY_ORANGE"/>
|
||||
</Grid>
|
||||
</cwuc:Case>
|
||||
@@ -440,17 +443,10 @@
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:AutoHeightBehavior TargetHeight="1024" TargetWidth="2048"/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Margin="16"
|
||||
Background="{StaticResource CardBackgroundFillColorDefault}"
|
||||
BorderBrush="{StaticResource CardStrokeColorDefault}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource CompatCornerRadius}">
|
||||
<Border Margin="16" Style="{StaticResource BorderCardStyle}">
|
||||
<shci:CachedImage HorizontalAlignment="Stretch" Source="{Binding Selected.Icon, Converter={StaticResource GachaAvatarIconConverter}}"/>
|
||||
</Border>
|
||||
|
||||
|
||||
<shci:CachedImage
|
||||
Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -537,7 +533,7 @@
|
||||
Width="80"
|
||||
Height="80"
|
||||
Margin="16,0,16,16"
|
||||
Source="https://static.snapgenshin.com/AvatarCard/UI_AvatarIcon_Costume_Card.png"/>
|
||||
Source="{StaticResource UI_AvatarIcon_Costume_Card}"/>
|
||||
<shci:CachedImage
|
||||
Width="80"
|
||||
Height="80"
|
||||
@@ -604,20 +600,6 @@
|
||||
</SplitView.Content>
|
||||
</SplitView>
|
||||
</Grid>
|
||||
<cwuc:Loading IsLoading="{Binding Avatars, Converter={StaticResource EmptyCollectionToBoolRevertConverter}}">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<shci:CachedImage
|
||||
Width="120"
|
||||
Height="120"
|
||||
Source="https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png"/>
|
||||
<TextBlock
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="加载中,请稍候"/>
|
||||
<ProgressRing Margin="0,16,0,0" IsActive="True"/>
|
||||
</StackPanel>
|
||||
</cwuc:Loading>
|
||||
<shvc:LoadingView IsLoading="{Binding Avatars, Converter={StaticResource EmptyCollectionToBoolRevertConverter}}"/>
|
||||
</Grid>
|
||||
|
||||
</Page>
|
||||
@@ -73,7 +73,7 @@
|
||||
Grid.Row="1"
|
||||
DisplayMode="Inline"
|
||||
IsPaneOpen="True"
|
||||
OpenPaneLength="200"
|
||||
OpenPaneLength="{StaticResource CompatSplitViewOpenPaneLength}"
|
||||
PaneBackground="{StaticResource CardBackgroundFillColorSecondary}">
|
||||
<SplitView.Pane>
|
||||
<cwuc:SwitchPresenter Grid.Row="1" Value="{Binding ElementName=ItemsPanelSelector, Path=Current}">
|
||||
@@ -109,19 +109,18 @@
|
||||
<cwuc:Case Value="Grid">
|
||||
<GridView
|
||||
Margin="6,6,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
ItemsSource="{Binding Weapons}"
|
||||
SelectedItem="{Binding Selected, Mode=TwoWay}"
|
||||
SelectionMode="Single">
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="40"
|
||||
Height="40"
|
||||
Margin="0"
|
||||
Source="{Binding Icon, Converter={StaticResource EquipIconConverter}, Mode=OneWay}"/>
|
||||
<shvc:ItemIcon
|
||||
Width="44"
|
||||
Height="44"
|
||||
Icon="{Binding Icon, Converter={StaticResource EquipIconConverter}, Mode=OneWay}"
|
||||
Quality="{Binding Quality}"/>
|
||||
</DataTemplate>
|
||||
</GridView.ItemTemplate>
|
||||
</GridView>
|
||||
@@ -243,19 +242,6 @@
|
||||
</SplitView.Content>
|
||||
</SplitView>
|
||||
</Grid>
|
||||
<cwuc:Loading IsLoading="{Binding Weapons, Converter={StaticResource EmptyCollectionToBoolRevertConverter}}">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<shci:CachedImage
|
||||
Width="120"
|
||||
Height="120"
|
||||
Source="https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png"/>
|
||||
<TextBlock
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="加载中,请稍候"/>
|
||||
<ProgressRing Margin="0,16,0,0" IsActive="True"/>
|
||||
</StackPanel>
|
||||
</cwuc:Loading>
|
||||
<shvc:LoadingView IsLoading="{Binding Weapons, Converter={StaticResource EmptyCollectionToBoolRevertConverter}}"/>
|
||||
</Grid>
|
||||
</shc:ScopedPage>
|
||||
|
||||
60
src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml
Normal file
60
src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml
Normal file
@@ -0,0 +1,60 @@
|
||||
<UserControl
|
||||
x:Class="Snap.Hutao.View.WelcomeView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
xmlns:shv="using:Snap.Hutao.ViewModel"
|
||||
d:DataContext="{d:DesignInstance shv:WelcomeViewModel}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
|
||||
<Grid Margin="0,44,0,0">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,0" HorizontalAlignment="Left">
|
||||
<TextBlock Style="{StaticResource TitleTextBlockStyle}" Text="欢迎使用 胡桃"/>
|
||||
<TextBlock
|
||||
Margin="0,8,0,0"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="请勿关闭应用程序"/>
|
||||
<TextBlock
|
||||
Margin="0,8,0,0"
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="我们将为你下载最基本的图像资源"/>
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="你可以继续使用电脑,丝毫不受影响"/>
|
||||
<ItemsControl Margin="0,0,0,32" ItemsSource="{Binding DownloadSummaries}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="0,8,0,0">
|
||||
<TextBlock Text="{Binding DisplayName}"/>
|
||||
<ProgressBar
|
||||
Width="240"
|
||||
Margin="0,4,0,0"
|
||||
Maximum="1"
|
||||
Value="{Binding ProgressValue}"/>
|
||||
<TextBlock
|
||||
Opacity="0.6"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding Description}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<Image
|
||||
MaxWidth="640"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Source="ms-appx:///Resource/WelcomeView_Background.png"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
22
src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml.cs
Normal file
22
src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.ViewModel;
|
||||
|
||||
namespace Snap.Hutao.View;
|
||||
|
||||
/// <summary>
|
||||
/// <20><>ӭ<EFBFBD><D3AD>ͼ
|
||||
/// </summary>
|
||||
public sealed partial class WelcomeView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µĻ<C2B5>ӭ<EFBFBD><D3AD>ͼ
|
||||
/// </summary>
|
||||
public WelcomeView()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = Ioc.Default.GetRequiredService<WelcomeViewModel>();
|
||||
}
|
||||
}
|
||||
@@ -301,26 +301,32 @@ internal class AchievementViewModel
|
||||
#region 存档操作
|
||||
private async Task AddArchiveAsync()
|
||||
{
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
(bool isOk, string name) = await new AchievementArchiveCreateDialog(mainWindow).GetInputAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
if (Archives != null)
|
||||
{
|
||||
ArchiveAddResult result = await achievementService.TryAddArchiveAsync(Model.Entity.AchievementArchive.Create(name)).ConfigureAwait(false);
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
(bool isOk, string name) = await new AchievementArchiveCreateDialog(mainWindow).GetInputAsync().ConfigureAwait(false);
|
||||
|
||||
switch (result)
|
||||
if (isOk)
|
||||
{
|
||||
case ArchiveAddResult.Added:
|
||||
infoBarService.Success($"存档 [{name}] 添加成功");
|
||||
break;
|
||||
case ArchiveAddResult.InvalidName:
|
||||
infoBarService.Information($"不能添加名称无效的存档");
|
||||
break;
|
||||
case ArchiveAddResult.AlreadyExists:
|
||||
infoBarService.Information($"不能添加名称重复的存档 [{name}]");
|
||||
break;
|
||||
default:
|
||||
throw Must.NeverHappen();
|
||||
ArchiveAddResult result = await achievementService.TryAddArchiveAsync(Model.Entity.AchievementArchive.Create(name)).ConfigureAwait(false);
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case ArchiveAddResult.Added:
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SelectedArchive = Archives.SingleOrDefault(a => a.Name == name);
|
||||
|
||||
infoBarService.Success($"存档 [{name}] 添加成功");
|
||||
break;
|
||||
case ArchiveAddResult.InvalidName:
|
||||
infoBarService.Information($"不能添加名称无效的存档");
|
||||
break;
|
||||
case ArchiveAddResult.AlreadyExists:
|
||||
infoBarService.Information($"不能添加名称重复的存档 [{name}]");
|
||||
break;
|
||||
default:
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,7 +351,8 @@ internal class AchievementViewModel
|
||||
{
|
||||
await achievementService.RemoveArchiveAsync(SelectedArchive).ConfigureAwait(false);
|
||||
|
||||
// reselect first archive
|
||||
// Re-select first archive
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SelectedArchive = Archives.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
/// </summary>
|
||||
public ICommand OpenDataFolderCommand { get; }
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 清空用户命令
|
||||
/// </summary>
|
||||
@@ -66,7 +64,7 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
|
||||
private Task OpenCacheFolderAsync()
|
||||
{
|
||||
return Launcher.LaunchFolderAsync(ApplicationData.Current.TemporaryFolder).AsTask();
|
||||
return Launcher.LaunchFolderAsync(ApplicationData.Current.LocalCacheFolder).AsTask();
|
||||
}
|
||||
|
||||
private Task OpenDataFolderAsync()
|
||||
@@ -74,8 +72,6 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
return Launcher.LaunchFolderPathAsync(hutaoLocation.GetPath()).AsTask();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task DangerousDeleteUsersAsync()
|
||||
{
|
||||
using (IServiceScope scope = Ioc.Default.CreateScope())
|
||||
|
||||
@@ -269,7 +269,8 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
|
||||
private void SaveSetting()
|
||||
{
|
||||
DbSet<SettingEntry> settings = appDbContext.Settings;
|
||||
settings.SingleOrAdd(SettingEntry.LaunchIsFullScreen, TrueString).SetBoolean(IsFullScreen);
|
||||
settings.SingleOrAdd(SettingEntry.LaunchIsExclusive, FalseString).SetBoolean(IsExclusive);
|
||||
settings.SingleOrAdd(SettingEntry.LaunchIsFullScreen, FalseString).SetBoolean(IsFullScreen);
|
||||
settings.SingleOrAdd(SettingEntry.LaunchIsBorderless, FalseString).SetBoolean(IsBorderless);
|
||||
settings.SingleOrAdd(SettingEntry.LaunchScreenWidth, "1920").SetInt32(ScreenWidth);
|
||||
settings.SingleOrAdd(SettingEntry.LaunchScreenHeight, "1080").SetInt32(ScreenHeight);
|
||||
|
||||
@@ -185,37 +185,6 @@ internal class SettingViewModel : ObservableObject
|
||||
/// </summary>
|
||||
public ICommand ShowSignInWebViewDialogCommand { get; }
|
||||
|
||||
private static async Task DangerousUnusedLoginMethodAsync()
|
||||
{
|
||||
LoginMihoyoBBSDialog dialog = ActivatorUtilities.CreateInstance<LoginMihoyoBBSDialog>(Ioc.Default);
|
||||
(bool isOk, Dictionary<string, string>? data) = await dialog.GetInputAccountPasswordAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
(Response<LoginResult>? resp, Aigis? aigis) = await Ioc.Default
|
||||
.GetRequiredService<PassportClient2>()
|
||||
.LoginByPasswordAsync(data, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (resp != null)
|
||||
{
|
||||
if (resp.IsOk())
|
||||
{
|
||||
Cookie cookie = Cookie.FromLoginResult(resp.Data);
|
||||
|
||||
await Ioc.Default
|
||||
.GetRequiredService<IUserService>()
|
||||
.ProcessInputCookieAsync(cookie)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (resp.ReturnCode == (int)KnownReturnCode.RET_NEED_AIGIS)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetGamePathAsync()
|
||||
{
|
||||
IGameLocator locator = Ioc.Default.GetRequiredService<IEnumerable<IGameLocator>>()
|
||||
@@ -252,8 +221,10 @@ internal class SettingViewModel : ObservableObject
|
||||
private async Task DebugThrowExceptionAsync()
|
||||
{
|
||||
#if DEBUG
|
||||
CommunityGameRecordDialog dialog = ActivatorUtilities.CreateInstance<CommunityGameRecordDialog>(Ioc.Default);
|
||||
await dialog.ShowAsync();
|
||||
await Ioc.Default
|
||||
.GetRequiredService<Service.Navigation.INavigationService>()
|
||||
.NavigateAsync<View.Page.TestPage>(Service.Navigation.INavigationAwaiter.Default)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await Task.Yield();
|
||||
#endif
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI.UI;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Message;
|
||||
using Snap.Hutao.Model.Binding.Cultivation;
|
||||
using Snap.Hutao.Model.Binding.Hutao;
|
||||
using Snap.Hutao.Model.Binding.SpiralAbyss;
|
||||
@@ -38,7 +40,7 @@ namespace Snap.Hutao.ViewModel;
|
||||
/// 深渊记录视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellation
|
||||
internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellation, IRecipient<UserChangedMessage>
|
||||
{
|
||||
private readonly ISpiralAbyssRecordService spiralAbyssRecordService;
|
||||
private readonly IMetadataService metadataService;
|
||||
@@ -56,11 +58,13 @@ internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellati
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
/// <param name="userService">用户服务</param>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public SpiralAbyssRecordViewModel(
|
||||
ISpiralAbyssRecordService spiralAbyssRecordService,
|
||||
IMetadataService metadataService,
|
||||
IUserService userService,
|
||||
IAsyncRelayCommandFactory asyncRelayCommandFactory)
|
||||
IAsyncRelayCommandFactory asyncRelayCommandFactory,
|
||||
IMessenger messenger)
|
||||
{
|
||||
this.spiralAbyssRecordService = spiralAbyssRecordService;
|
||||
this.metadataService = metadataService;
|
||||
@@ -69,6 +73,8 @@ internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellati
|
||||
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
||||
RefreshCommand = asyncRelayCommandFactory.Create(RefreshAsync);
|
||||
UploadSpiralAbyssRecordCommand = asyncRelayCommandFactory.Create(UploadSpiralAbyssRecordAsync);
|
||||
|
||||
messenger.Register(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -116,20 +122,46 @@ internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellati
|
||||
/// </summary>
|
||||
public ICommand UploadSpiralAbyssRecordCommand { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Receive(UserChangedMessage message)
|
||||
{
|
||||
if (message.NewValue != null)
|
||||
{
|
||||
UserAndRole userAndRole = UserAndRole.FromUser(message.NewValue);
|
||||
if (userAndRole.Role != null)
|
||||
{
|
||||
UpdateSpiralAbyssCollectionAsync(UserAndRole.FromUser(message.NewValue)).SafeForget();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SpiralAbyssView = null;
|
||||
}
|
||||
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
idAvatarMap = AvatarIds.ExtendAvatars(idAvatarMap);
|
||||
ObservableCollection<SpiralAbyssEntry> temp = await spiralAbyssRecordService.GetSpiralAbyssCollectionAsync().ConfigureAwait(false);
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SpiralAbyssEntries = temp;
|
||||
SelectedEntry = SpiralAbyssEntries.FirstOrDefault();
|
||||
if (userService.Current?.SelectedUserGameRole != null)
|
||||
{
|
||||
await UpdateSpiralAbyssCollectionAsync(UserAndRole.FromUser(userService.Current)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateSpiralAbyssCollectionAsync(UserAndRole userAndRole)
|
||||
{
|
||||
ObservableCollection<SpiralAbyssEntry> temp = await spiralAbyssRecordService
|
||||
.GetSpiralAbyssCollectionAsync(userAndRole)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SpiralAbyssEntries = temp;
|
||||
SelectedEntry = SpiralAbyssEntries.FirstOrDefault();
|
||||
}
|
||||
|
||||
private async Task RefreshAsync()
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
@@ -139,6 +171,9 @@ internal class SpiralAbyssRecordViewModel : ObservableObject, ISupportCancellati
|
||||
await spiralAbyssRecordService
|
||||
.RefreshSpiralAbyssAsync(UserAndRole.FromUser(userService.Current))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SelectedEntry = SpiralAbyssEntries?.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
127
src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs
Normal file
127
src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI.UI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.Bits;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Model.Binding.Cultivation;
|
||||
using Snap.Hutao.Model.Binding.Hutao;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Weapon;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Cultivation;
|
||||
using Snap.Hutao.Service.Hutao;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Passport;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Immutable;
|
||||
using CalcAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
|
||||
using CalcClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
|
||||
using CalcConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
|
||||
|
||||
namespace Snap.Hutao.ViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 测试视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class TestViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的测试视图模型
|
||||
/// </summary>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
public TestViewModel(IAsyncRelayCommandFactory asyncRelayCommandFactory)
|
||||
{
|
||||
ShowCommunityGameRecordDialogCommand = asyncRelayCommandFactory.Create(ShowCommunityGameRecordDialogAsync);
|
||||
DangerousLoginMihoyoBbsCommand = asyncRelayCommandFactory.Create(DangerousLoginMihoyoBbsAsync);
|
||||
DownloadStaticFileCommand = asyncRelayCommandFactory.Create(DownloadStaticFileAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 打开游戏社区记录对话框命令
|
||||
/// </summary>
|
||||
public ICommand ShowCommunityGameRecordDialogCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Dangerous 登录米游社命令
|
||||
/// </summary>
|
||||
public ICommand DangerousLoginMihoyoBbsCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 下载资源文件命令
|
||||
/// </summary>
|
||||
public ICommand DownloadStaticFileCommand { get; }
|
||||
|
||||
private async Task ShowCommunityGameRecordDialogAsync()
|
||||
{
|
||||
CommunityGameRecordDialog dialog = ActivatorUtilities.CreateInstance<CommunityGameRecordDialog>(Ioc.Default);
|
||||
await dialog.ShowAsync();
|
||||
}
|
||||
|
||||
private async Task DangerousLoginMihoyoBbsAsync()
|
||||
{
|
||||
LoginMihoyoBBSDialog dialog = ActivatorUtilities.CreateInstance<LoginMihoyoBBSDialog>(Ioc.Default);
|
||||
(bool isOk, Dictionary<string, string>? data) = await dialog.GetInputAccountPasswordAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
(Response<LoginResult>? resp, Aigis? aigis) = await Ioc.Default
|
||||
.GetRequiredService<PassportClient2>()
|
||||
.LoginByPasswordAsync(data, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (resp != null)
|
||||
{
|
||||
if (resp.IsOk())
|
||||
{
|
||||
Cookie cookie = Cookie.FromLoginResult(resp.Data);
|
||||
|
||||
await Ioc.Default
|
||||
.GetRequiredService<IUserService>()
|
||||
.ProcessInputCookieAsync(cookie)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (resp.ReturnCode == (int)KnownReturnCode.RET_NEED_AIGIS)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadStaticFileAsync()
|
||||
{
|
||||
BitsManager bitsManager = Ioc.Default.GetRequiredService<BitsManager>();
|
||||
Uri testUri = new(Web.HutaoEndpoints.StaticZip("AvatarIcon"));
|
||||
ILogger<TestViewModel> logger = Ioc.Default.GetRequiredService<ILogger<TestViewModel>>();
|
||||
Progress<ProgressUpdateStatus> progress = new(status => logger.LogInformation("{info}", status));
|
||||
(bool isOk, TempFile file) = await bitsManager.DownloadAsync(testUri, progress).ConfigureAwait(false);
|
||||
|
||||
using (file)
|
||||
{
|
||||
if (isOk)
|
||||
{
|
||||
logger.LogInformation("Download completed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Download failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
174
src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
Normal file
174
src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Common;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI.Notifications;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.Bits;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace Snap.Hutao.ViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 欢迎视图模型
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class WelcomeViewModel : ObservableObject
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private ObservableCollection<DownloadSummary>? downloadSummaries;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的欢迎视图模型
|
||||
/// </summary>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public WelcomeViewModel(IAsyncRelayCommandFactory asyncRelayCommandFactory, IServiceProvider serviceProvider)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载信息
|
||||
/// </summary>
|
||||
public ObservableCollection<DownloadSummary>? DownloadSummaries { get => downloadSummaries; set => SetProperty(ref downloadSummaries, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 打开界面命令
|
||||
/// </summary>
|
||||
public ICommand OpenUICommand { get; }
|
||||
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
List<DownloadSummary> downloadSummaries = new()
|
||||
{
|
||||
new(serviceProvider, "基础图标", "Bg"),
|
||||
new(serviceProvider, "角色图标", "AvatarIcon"),
|
||||
new(serviceProvider, "角色立绘图标", "GachaAvatarIcon"),
|
||||
new(serviceProvider, "角色立绘图像", "GachaAvatarImg"),
|
||||
new(serviceProvider, "武器图标", "EquipIcon"),
|
||||
new(serviceProvider, "武器立绘图标", "GachaEquipIcon"),
|
||||
new(serviceProvider, "名片图像", "NameCardPic"),
|
||||
new(serviceProvider, "天赋图标", "Skill"),
|
||||
new(serviceProvider, "命之座图标", "Talent"),
|
||||
};
|
||||
|
||||
DownloadSummaries = new(downloadSummaries);
|
||||
|
||||
await Task.WhenAll(downloadSummaries.Select(d => d.DownloadAndExtractAsync())).ConfigureAwait(true);
|
||||
|
||||
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
|
||||
|
||||
// Complete the StaticResourceV1Contract
|
||||
LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
|
||||
|
||||
new ToastContentBuilder()
|
||||
.AddText("下载完成")
|
||||
.AddText("现在可以开始使用胡桃了")
|
||||
.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载信息
|
||||
/// </summary>
|
||||
public class DownloadSummary : ObservableObject
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly BitsManager bitsManager;
|
||||
private readonly string fileName;
|
||||
private readonly Uri fileUri;
|
||||
private readonly Progress<ProgressUpdateStatus> progress;
|
||||
private string description = "等待中";
|
||||
private double progressValue;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的下载信息
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="displayName">显示名称</param>
|
||||
/// <param name="fileName">压缩文件名称</param>
|
||||
public DownloadSummary(IServiceProvider serviceProvider, string displayName, string fileName)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
bitsManager = serviceProvider.GetRequiredService<BitsManager>();
|
||||
DisplayName = displayName;
|
||||
this.fileName = fileName;
|
||||
fileUri = new(Web.HutaoEndpoints.StaticZip(fileName));
|
||||
|
||||
progress = new(UpdateProgressStatus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示名称
|
||||
/// </summary>
|
||||
public string DisplayName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 描述
|
||||
/// </summary>
|
||||
public string Description { get => description; private set => SetProperty(ref description, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 进度值,最大1
|
||||
/// </summary>
|
||||
public double ProgressValue { get => progressValue; set => SetProperty(ref progressValue, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 异步下载并解压
|
||||
/// </summary>
|
||||
/// <returns>任务</returns>
|
||||
public async Task DownloadAndExtractAsync()
|
||||
{
|
||||
(bool isOk, TempFile file) = await bitsManager.DownloadAsync(fileUri, progress).ConfigureAwait(false);
|
||||
|
||||
using (file)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
if (isOk && File.Exists(file.Path))
|
||||
{
|
||||
ProgressValue = 1;
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
ExtractFiles(file.Path);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
Description = "完成";
|
||||
}
|
||||
else
|
||||
{
|
||||
ProgressValue = 0;
|
||||
Description = "文件下载异常";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateProgressStatus(ProgressUpdateStatus status)
|
||||
{
|
||||
Description = $"{Converters.ToFileSizeString(status.BytesRead)}/{Converters.ToFileSizeString(status.TotalBytes)}";
|
||||
ProgressValue = (double)status.BytesRead / status.TotalBytes;
|
||||
}
|
||||
|
||||
private void ExtractFiles(string file)
|
||||
{
|
||||
IImageCacheFilePathOperation imageCache = serviceProvider.GetRequiredService<IImageCache>().ImplictAs<IImageCacheFilePathOperation>()!;
|
||||
|
||||
using (ZipArchive archive = ZipFile.OpenRead(file))
|
||||
{
|
||||
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||
{
|
||||
string destPath = imageCache.GetFilePathFromCategoryAndFileName(fileName, entry.FullName);
|
||||
entry.ExtractToFile(destPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,4 +231,4 @@ internal class WikiWeaponViewModel : ObservableObject, ISupportCancellation
|
||||
return keep;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using Snap.Hutao.Web.Hoyolab;
|
||||
namespace Snap.Hutao.Web;
|
||||
|
||||
/// <summary>
|
||||
/// API端点
|
||||
/// API 端点
|
||||
/// </summary>
|
||||
[SuppressMessage("", "SA1201")]
|
||||
[SuppressMessage("", "SA1124")]
|
||||
@@ -311,14 +311,6 @@ internal static class ApiEndpoints
|
||||
public const string AccountCreateActionTicket = $"{PassportApi}/account/ma-cn-verifier/app/createActionTicketByToken";
|
||||
#endregion
|
||||
|
||||
#region Patcher
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃检查更新
|
||||
/// </summary>
|
||||
public const string PatcherHutaoStable = $"{PatcherApi}/hutao/stable";
|
||||
#endregion
|
||||
|
||||
#region SdkStaticLauncherApi
|
||||
|
||||
/// <summary>
|
||||
@@ -368,8 +360,6 @@ internal static class ApiEndpoints
|
||||
private const string PassportApiAuthApi = $"{PassportApi}/account/auth/api";
|
||||
private const string PassportApiV4 = "https://passport-api-v4.mihoyo.com";
|
||||
|
||||
private const string PatcherApi = "https://patcher.dgp-studio.cn";
|
||||
|
||||
private const string SdkStatic = "https://sdk-static.mihoyo.com";
|
||||
private const string SdkStaticLauncherApi = $"{SdkStatic}/hk4e_cn/mdk/launcher/api";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Snap.Hutao.Web.Enka;
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class EnkaClient
|
||||
{
|
||||
private const string EnkaAPI = "https://enka.shinshin.moe/u/{0}/__data.json";
|
||||
private const string EnkaAPI = "https://enka.network/u/{0}/__data.json";
|
||||
private const string EnkaAPIHutaoForward = "https://enka-api.hut.ao/{0}";
|
||||
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -22,8 +22,6 @@ namespace Snap.Hutao.Web.Hutao;
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class HomaClient
|
||||
{
|
||||
private const string HutaoAPI = "https://homa.snapgenshin.com";
|
||||
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly GameRecordClient gameRecordClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
@@ -54,7 +52,7 @@ internal class HomaClient
|
||||
public async Task<bool> CheckRecordUploadedAsync(PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
Response<bool>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<bool>>($"{HutaoAPI}/Record/Check?uid={uid}", token)
|
||||
.GetFromJsonAsync<Response<bool>>(HutaoEndpoints.RecordCheck(uid.Value), token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data == true;
|
||||
@@ -70,8 +68,8 @@ internal class HomaClient
|
||||
public async Task<RankInfo?> GetRankAsync(PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
Response<RankInfo>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<RankInfo>>($"{HutaoAPI}/Record/Rank?uid={uid}", token)
|
||||
.ConfigureAwait(false);
|
||||
.GetFromJsonAsync<Response<RankInfo>>(HutaoEndpoints.RecordRank(uid.Value), token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
}
|
||||
@@ -85,7 +83,7 @@ internal class HomaClient
|
||||
public async Task<Overview?> GetOverviewAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<Overview>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<Overview>>($"{HutaoAPI}/Statistics/Overview", token)
|
||||
.GetFromJsonAsync<Response<Overview>>(HutaoEndpoints.StatisticsOverview, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
@@ -100,7 +98,7 @@ internal class HomaClient
|
||||
public async Task<List<AvatarAppearanceRank>> GetAvatarAttendanceRatesAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<List<AvatarAppearanceRank>>? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<List<AvatarAppearanceRank>>>($"{HutaoAPI}/Statistics/Avatar/AttendanceRate", options, logger, token)
|
||||
.TryCatchGetFromJsonAsync<Response<List<AvatarAppearanceRank>>>(HutaoEndpoints.StatisticsAvatarAttendanceRate, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
@@ -115,7 +113,7 @@ internal class HomaClient
|
||||
public async Task<List<AvatarUsageRank>> GetAvatarUtilizationRatesAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<List<AvatarUsageRank>>? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<List<AvatarUsageRank>>>($"{HutaoAPI}/Statistics/Avatar/UtilizationRate", options, logger, token)
|
||||
.TryCatchGetFromJsonAsync<Response<List<AvatarUsageRank>>>(HutaoEndpoints.StatisticsAvatarUtilizationRate, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
@@ -130,7 +128,7 @@ internal class HomaClient
|
||||
public async Task<List<AvatarCollocation>> GetAvatarCollocationsAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<List<AvatarCollocation>>? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<List<AvatarCollocation>>>($"{HutaoAPI}/Statistics/Avatar/AvatarCollocation", options, logger, token)
|
||||
.TryCatchGetFromJsonAsync<Response<List<AvatarCollocation>>>(HutaoEndpoints.StatisticsAvatarAvatarCollocation, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
@@ -145,7 +143,7 @@ internal class HomaClient
|
||||
public async Task<List<WeaponCollocation>> GetWeaponCollocationsAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<List<WeaponCollocation>>? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<List<WeaponCollocation>>>($"{HutaoAPI}/Statistics/Weapon/WeaponCollocation", options, logger, token)
|
||||
.TryCatchGetFromJsonAsync<Response<List<WeaponCollocation>>>(HutaoEndpoints.StatisticsWeaponWeaponCollocation, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
@@ -160,7 +158,7 @@ internal class HomaClient
|
||||
public async Task<List<AvatarConstellationInfo>> GetAvatarHoldingRatesAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<List<AvatarConstellationInfo>>? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<List<AvatarConstellationInfo>>>($"{HutaoAPI}/Statistics/Avatar/HoldingRate", options, logger, token)
|
||||
.TryCatchGetFromJsonAsync<Response<List<AvatarConstellationInfo>>>(HutaoEndpoints.StatisticsAvatarHoldingRate, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
@@ -175,7 +173,7 @@ internal class HomaClient
|
||||
public async Task<List<TeamAppearance>> GetTeamCombinationsAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<List<TeamAppearance>>? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<List<TeamAppearance>>>($"{HutaoAPI}/Statistics/Team/Combination", options, logger, token)
|
||||
.TryCatchGetFromJsonAsync<Response<List<TeamAppearance>>>(HutaoEndpoints.StatisticsTeamCombination, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
@@ -217,6 +215,6 @@ internal class HomaClient
|
||||
/// <returns>响应</returns>
|
||||
public Task<Response<string>?> UploadRecordAsync(SimpleRecord playerRecord, CancellationToken token = default)
|
||||
{
|
||||
return httpClient.TryCatchPostAsJsonAsync<SimpleRecord, Response<string>>($"{HutaoAPI}/Record/Upload", playerRecord, options, logger, token);
|
||||
return httpClient.TryCatchPostAsJsonAsync<SimpleRecord, Response<string>>(HutaoEndpoints.RecordUpload, playerRecord, options, logger, token);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ namespace Snap.Hutao.Web.Hutao;
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class HomaClient2
|
||||
{
|
||||
private const string HutaoAPI = "https://homa.snapgenshin.com";
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
/// <summary>
|
||||
@@ -41,7 +40,7 @@ internal class HomaClient2
|
||||
};
|
||||
|
||||
Response<string>? a = await httpClient
|
||||
.TryCatchPostAsJsonAsync<HutaoLog, Response<string>>($"{HutaoAPI}/HutaoLog/Upload", log)
|
||||
.TryCatchPostAsJsonAsync<HutaoLog, Response<string>>(HutaoEndpoints.HutaoLogUpload, log)
|
||||
.ConfigureAwait(false);
|
||||
return a?.Data;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,6 @@ internal class PatchClient
|
||||
/// <returns>更新信息</returns>
|
||||
public Task<Patch.PatchInformation?> GetPatchInformationAsync(CancellationToken token = default)
|
||||
{
|
||||
return httpClient.TryCatchGetFromJsonAsync<Patch.PatchInformation>(ApiEndpoints.PatcherHutaoStable, options, logger, token);
|
||||
return httpClient.TryCatchGetFromJsonAsync<Patch.PatchInformation>(HutaoEndpoints.PatcherHutaoStable, options, logger, token);
|
||||
}
|
||||
}
|
||||
146
src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs
Normal file
146
src/Snap.Hutao/Snap.Hutao/Web/HutaoEndpoints.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃 API 端点
|
||||
/// </summary>
|
||||
[SuppressMessage("", "SA1201")]
|
||||
[SuppressMessage("", "SA1124")]
|
||||
internal static class HutaoEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// 胡桃资源主机名
|
||||
/// </summary>
|
||||
public const string StaticHutao = "static.hut.ao";
|
||||
|
||||
#region HutaoAPI
|
||||
|
||||
/// <summary>
|
||||
/// 上传日志
|
||||
/// </summary>
|
||||
public const string HutaoLogUpload = $"{HomaSnapGenshinApi}/HutaoLog/Upload";
|
||||
|
||||
/// <summary>
|
||||
/// 检查 uid 是否上传记录
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>路径</returns>
|
||||
public static string RecordCheck(string uid)
|
||||
{
|
||||
return $"{HomaSnapGenshinApi}/Record/Check?uid={uid}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// uid 排行
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>路径</returns>
|
||||
public static string RecordRank(string uid)
|
||||
{
|
||||
return $"{HomaSnapGenshinApi}/Record/Rank?uid={uid}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传记录
|
||||
/// </summary>
|
||||
public const string RecordUpload = $"{HomaSnapGenshinApi}/Record/Upload";
|
||||
|
||||
/// <summary>
|
||||
/// 统计信息
|
||||
/// </summary>
|
||||
public const string StatisticsOverview = $"{HomaSnapGenshinApi}/Statistics/Overview";
|
||||
|
||||
/// <summary>
|
||||
/// 出场率
|
||||
/// </summary>
|
||||
public const string StatisticsAvatarAttendanceRate = $"{HomaSnapGenshinApi}/Statistics/Avatar/AttendanceRate";
|
||||
|
||||
/// <summary>
|
||||
/// 使用率
|
||||
/// </summary>
|
||||
public const string StatisticsAvatarUtilizationRate = $"{HomaSnapGenshinApi}/Statistics/Avatar/UtilizationRate";
|
||||
|
||||
/// <summary>
|
||||
/// 角色搭配
|
||||
/// </summary>
|
||||
public const string StatisticsAvatarAvatarCollocation = $"{HomaSnapGenshinApi}/Statistics/Avatar/AvatarCollocation";
|
||||
|
||||
/// <summary>
|
||||
/// 角色持有率
|
||||
/// </summary>
|
||||
public const string StatisticsAvatarHoldingRate = $"{HomaSnapGenshinApi}/Statistics/Avatar/HoldingRate";
|
||||
|
||||
/// <summary>
|
||||
/// 武器搭配
|
||||
/// </summary>
|
||||
public const string StatisticsWeaponWeaponCollocation = $"{HomaSnapGenshinApi}/Statistics/Weapon/WeaponCollocation";
|
||||
|
||||
/// <summary>
|
||||
/// 持有率
|
||||
/// </summary>
|
||||
public const string StatisticsTeamCombination = $"{HomaSnapGenshinApi}/Statistics/Team/Combination";
|
||||
#endregion
|
||||
|
||||
#region Metadata
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃元数据文件
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名称</param>
|
||||
/// <returns>路径</returns>
|
||||
public static string HutaoMetadataFile(string fileName)
|
||||
{
|
||||
return $"{HutaoMetadataSnapGenshinApi}/{fileName}";
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Patcher
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃检查更新
|
||||
/// </summary>
|
||||
public const string PatcherHutaoStable = $"{PatcherDGPStudioApi}/hutao/stable";
|
||||
#endregion
|
||||
|
||||
#region Static & Zip
|
||||
|
||||
/// <summary>
|
||||
/// UI_Icon_None
|
||||
/// </summary>
|
||||
public static readonly Uri UIIconNone = new(StaticFile("Bg", "UI_Icon_None.png"));
|
||||
|
||||
/// <summary>
|
||||
/// UI_ItemIcon_None
|
||||
/// </summary>
|
||||
public static readonly Uri UIItemIconNone = new(StaticFile("Bg", "UI_ItemIcon_None.png"));
|
||||
|
||||
/// <summary>
|
||||
/// 压缩包资源
|
||||
/// </summary>
|
||||
/// <param name="category">分类</param>
|
||||
/// <param name="fileName">文件名称 包括后缀</param>
|
||||
/// <returns>路径</returns>
|
||||
public static string StaticFile(string category, string fileName)
|
||||
{
|
||||
return $"{StaticSnapGenshinApi}/{category}/{fileName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 压缩包资源
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名称 不包括后缀</param>
|
||||
/// <returns>路径</returns>
|
||||
public static string StaticZip(string fileName)
|
||||
{
|
||||
return $"{StaticZipSnapGenshinApi}/{fileName}.zip";
|
||||
}
|
||||
#endregion
|
||||
|
||||
private const string HutaoMetadataSnapGenshinApi = "http://hutao-metadata.snapgenshin.com";
|
||||
private const string HomaSnapGenshinApi = "https://homa.snapgenshin.com";
|
||||
private const string PatcherDGPStudioApi = "https://patcher.dgp-studio.cn";
|
||||
private const string StaticSnapGenshinApi = "https://static.snapgenshin.com";
|
||||
private const string StaticZipSnapGenshinApi = "https://static-zip.snapgenshin.com";
|
||||
}
|
||||
@@ -12,24 +12,7 @@ namespace Snap.Hutao.Win32;
|
||||
internal static class MemoryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 暂时固定 <see cref="Span{T}"/> 以读取偏移量位置处的内存内容
|
||||
/// </summary>
|
||||
/// <typeparam name="T">输出类型</typeparam>
|
||||
/// <param name="span">内存</param>
|
||||
/// <param name="offset">偏移量</param>
|
||||
/// <returns>内容</returns>
|
||||
public static unsafe T Fixed<T>(this ref Span<byte> span, int offset)
|
||||
where T : unmanaged
|
||||
{
|
||||
fixed (byte* pSpan = span)
|
||||
{
|
||||
T result = *(T*)(pSpan + offset);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 __winmdroot_Foundation_CHAR_256 转换到 字符串
|
||||
/// 将 __CHAR_256 转换到 字符串
|
||||
/// </summary>
|
||||
/// <param name="char256">目标字符数组</param>
|
||||
/// <returns>结果字符串</returns>
|
||||
|
||||
Reference in New Issue
Block a user