Compare commits

..

2 Commits
1.3.3 ... 1.3.4

Author SHA1 Message Date
DismissedLight
650b67bea0 download static resource at startup 2023-01-07 18:27:45 +08:00
Masterain
18b3d23b1c Update azure-pipelines.yml for Azure Pipelines
- optimize CI logic for RPs
2023-01-03 19:17:14 -08:00
96 changed files with 1798 additions and 675 deletions

View File

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

View File

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

View File

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

View File

@@ -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)

View File

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

View File

@@ -9,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());
}
}

View File

@@ -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);

View File

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

View File

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

View File

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

View File

@@ -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();
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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";
}

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Message;
using Windows.Graphics;
using Windows.Win32.UI.WindowsAndMessaging;
@@ -13,7 +16,7 @@ namespace Snap.Hutao;
/// </summary>
[Injection(InjectAs.Singleton)]
[SuppressMessage("", "CA1001")]
public sealed partial class MainWindow : Window, IExtendedWindowSource
public sealed partial class MainWindow : Window, IExtendedWindowSource, IRecipient<WelcomeStateCompleteMessage>
{
private const int MinWidth = 848;
private const int MinHeight = 524;
@@ -27,6 +30,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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

View File

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

View File

@@ -0,0 +1,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);
}
}

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ internal class GameService : IGameService, IDisposable
{
IEnumerable<IGameLocator> gameLocators = scope.ServiceProvider.GetRequiredService<IEnumerable<IGameLocator>>();
// Try locate by registry
// Try locate by unity log
IGameLocator locator = gameLocators.Single(l => l.Name == nameof(UnityLogGameLocator));
ValueResult<bool, string> result = await locator.LocateGamePathAsync().ConfigureAwait(false);

View File

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

View File

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

View File

@@ -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

View File

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

View File

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

View File

@@ -64,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>

View File

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

View File

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

View File

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

View File

@@ -52,11 +52,12 @@
Visibility="{Binding IsUp, Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock
Width="20"
Width="24"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding LastPull}"
TextAlignment="Center"/>
TextAlignment="Center"
TextWrapping="NoWrap"/>
</StackPanel>
</Grid>
</DataTemplate>

View File

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

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -73,7 +73,7 @@
Description="Github 上反馈的问题会优先处理"
Header="反馈"
Icon="&#xED15;">
<HyperlinkButton Content="前往反馈" NavigateUri="https://hut.ao/statements/bug-report.html"/>
<HyperlinkButton Content="前往反馈" NavigateUri="{StaticResource DocumentLink_BugReport}"/>
</wsc:Setting>
<wsc:SettingExpander>
<wsc:SettingExpander.Header>

View File

@@ -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"

View 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>

View 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();
}
}

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>();
}
}

View File

@@ -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();
}
}

View File

@@ -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())

View File

@@ -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);

View File

@@ -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

View File

@@ -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();
}
}
}

View 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.");
}
}
}
}

View 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);
}
}
}
}
}

View File

@@ -231,4 +231,4 @@ internal class WikiWeaponViewModel : ObservableObject, ISupportCancellation
return keep;
}
}
}
}

View File

@@ -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";

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View 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";
}

View File

@@ -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>