update avatar page style

This commit is contained in:
DismissedLight
2022-07-14 22:44:47 +08:00
parent 3d87246c9e
commit 93da7cdac4
51 changed files with 1017 additions and 443 deletions

View File

@@ -1,2 +1,2 @@
# Snap.Hutao
Snap Genshin but WinUI3
唷,找本堂主有何贵干呀?

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.VisualStudio.Threading;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
@@ -68,6 +69,7 @@ public partial class App : Application
Window = Ioc.Default.GetRequiredService<MainWindow>();
Window.Activate();
logger.LogInformation("Image cache folder : {folder}", Windows.Storage.ApplicationData.Current.TemporaryFolder.Path);
if (Ioc.Default.GetRequiredService<IMetadataService>() is IMetadataInitializer initializer)
{
initializer.InitializeInternalAsync().SafeForget();

View File

@@ -17,11 +17,6 @@ public class AppDbContextDesignTimeFactory : IDesignTimeDbContextFactory<AppDbCo
public AppDbContext CreateDbContext(string[] args)
{
MyDocumentContext myDocument = new(new());
myDocument.EnsureDirectory();
string dbFile = myDocument.Locate("Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
return AppDbContext.Create(sqlConnectionString);
return AppDbContext.Create($"Data Source={myDocument.Locate("Userdata.db")}");
}
}

View File

@@ -21,6 +21,7 @@ internal abstract class FileSystemContext
public FileSystemContext(IFileSystemLocation location)
{
this.location = location;
EnsureDirectory();
}
/// <summary>
@@ -62,22 +63,6 @@ internal abstract class FileSystemContext
}
}
/// <summary>
/// 检查根目录
/// </summary>
/// <returns>是否创建了路径</returns>
public bool EnsureDirectory()
{
string folder = location.GetPath();
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
return true;
}
return false;
}
/// <summary>
/// 检查文件是否存在
/// </summary>
@@ -170,4 +155,20 @@ internal abstract class FileSystemContext
{
return File.Create(Locate(file));
}
/// <summary>
/// 检查根目录
/// </summary>
/// <returns>是否创建了路径</returns>
private bool EnsureDirectory()
{
string folder = location.GetPath();
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
return true;
}
return false;
}
}

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.HostBackdrop;
/// <summary>
/// 回退行为
/// </summary>
public enum BackbdropFallBackBehavior
{
/// <summary>
/// 回退到无
/// </summary>
None,
/// <summary>
/// 回退到亚克力
/// </summary>
Acrylic,
}

View File

@@ -16,10 +16,9 @@ namespace Snap.Hutao.Control.HostBackdrop;
public class SystemBackdrop
{
private readonly Window window;
private readonly BackbdropFallBackBehavior fallBackBehavior;
private WindowsSystemDispatcherQueueHelper? dispatcherQueueHelper;
private ISystemBackdropControllerWithTargets? backdropController;
private MicaController? backdropController;
private SystemBackdropConfiguration? configurationSource;
/// <summary>
@@ -27,16 +26,14 @@ public class SystemBackdrop
/// </summary>
/// <param name="window">窗体</param>
/// <param name="fallBackBehavior">回退行为</param>
public SystemBackdrop(Window window, BackbdropFallBackBehavior fallBackBehavior = BackbdropFallBackBehavior.Acrylic)
public SystemBackdrop(Window window)
{
this.window = window;
this.fallBackBehavior = fallBackBehavior;
}
private enum BackDropType
{
None,
Acrylic,
Mica,
}
@@ -46,9 +43,7 @@ public class SystemBackdrop
/// <returns>是否设置成功</returns>
public bool TrySetBackdrop()
{
BackDropType targetBackDropType = ResolveBackdropType();
if (targetBackDropType == BackDropType.None)
if (!MicaController.IsSupported())
{
return false;
}
@@ -67,12 +62,7 @@ public class SystemBackdrop
configurationSource.IsInputActive = true;
SetConfigurationSourceTheme();
backdropController = targetBackDropType switch
{
BackDropType.Mica => new MicaController(),
BackDropType.Acrylic => new DesktopAcrylicController(),
_ => throw Must.NeverHappen(),
};
backdropController = new MicaController();
ICompositionSupportsSystemBackdrop target = window.As<ICompositionSupportsSystemBackdrop>();
backdropController.AddSystemBackdropTarget(target);
@@ -82,27 +72,6 @@ public class SystemBackdrop
}
}
private BackDropType ResolveBackdropType()
{
BackDropType targetBackDropType = BackDropType.None;
if (MicaController.IsSupported())
{
targetBackDropType = BackDropType.Mica;
}
else
{
if (fallBackBehavior == BackbdropFallBackBehavior.Acrylic)
{
if (DesktopAcrylicController.IsSupported())
{
targetBackDropType = BackDropType.Acrylic;
}
}
}
return targetBackDropType;
}
private void WindowActivated(object sender, WindowActivatedEventArgs args)
{
Must.NotNull(configurationSource!);

View File

@@ -11,14 +11,14 @@ namespace Snap.Hutao.Control;
/// </summary>
public class BindingProxy : DependencyObject
{
private static readonly DependencyProperty DataProperty = Property<BindingProxy>.Depend<object>(nameof(DataContext));
private static readonly DependencyProperty DataContextProperty = Property<BindingProxy>.Depend<object>(nameof(DataContext));
/// <summary>
/// 数据上下文
/// </summary>
public object? DataContext
{
get => GetValue(DataProperty);
set => SetValue(DataProperty, value);
get => GetValue(DataContextProperty);
set => SetValue(DataContextProperty, value);
}
}
}

View File

@@ -32,16 +32,21 @@ public class CachedImage : ImageEx
/// <inheritdoc/>
protected override async Task<ImageSource> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
BitmapImage image;
BitmapImage? image;
try
{
image = await ImageCache.Instance.GetFromCacheAsync(imageUri, true, token);
}
catch (TaskCanceledException)
{
// task was explicitly canceled
throw;
}
catch
{
// maybe the image is corrupted remove it and re-download
// maybe the image is corrupted, remove it.
await ImageCache.Instance.RemoveAsync(imageUri.Enumerate());
image = await ImageCache.Instance.GetFromCacheAsync(imageUri, false, token);
throw;
}
// check token state to determine whether the operation should be canceled.

View File

@@ -0,0 +1,101 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// some part of this file came from:
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core;
using System.Linq;
using System.Text.RegularExpressions;
using Windows.UI;
namespace Snap.Hutao.Control.Text;
/// <summary>
/// 专用于呈现描述文本的文本块
/// </summary>
public class DescriptionTextBlock : ContentControl
{
private static readonly DependencyProperty DescriptionProperty =
Property<DescriptionTextBlock>.Depend(nameof(Description), string.Empty, OnDescriptionChanged);
/// <summary>
/// 构造一个新的呈现描述文本的文本块
/// </summary>
public DescriptionTextBlock()
{
Content = new TextBlock();
}
/// <summary>
/// 可绑定的描述文本
/// </summary>
public string Description
{
get => (string)GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBlock text = (TextBlock)((DescriptionTextBlock)d).Content;
text.Inlines.Clear();
string[] lines = ((string)e.NewValue).Split('\n');
foreach (string line in lines)
{
MatchCollection matches = Regex.Matches(line, @"<color=([^>]+)>([^<]+)</color>");
string left, right = line;
foreach (Match match in matches)
{
string matched = match.Groups[0].Value;
int matchPosition = right.IndexOf(matched);
left = right[..matchPosition];
right = right[(matchPosition + matched.Length)..];
if (!string.IsNullOrWhiteSpace(left))
{
text.Inlines.Add(new Run { Text = left });
}
string hexColor = match.Groups[1].Value;
string content = match.Groups[2].Value;
text.Inlines.Add(new Run { Text = content, Foreground = GetSolidColorBrush(hexColor[..7]) });
}
if (!string.IsNullOrWhiteSpace(right))
{
if (right.Contains("<i>"))
{
string italic = right.Replace("<i>", string.Empty).Replace("</i>", string.Empty);
text.Inlines.Add(new Run { Text = italic, FontStyle = Windows.UI.Text.FontStyle.Italic });
}
else
{
text.Inlines.Add(new Run { Text = right });
}
}
text.Inlines.Add(new LineBreak());
}
if (text.Inlines.LastOrDefault() is LineBreak newline)
{
text.Inlines.Remove(newline);
}
}
private static SolidColorBrush GetSolidColorBrush(string hex)
{
hex = hex.Replace("#", string.Empty);
byte r = (byte)Convert.ToUInt32(hex.Substring(0, 2), 16);
byte g = (byte)Convert.ToUInt32(hex.Substring(2, 2), 16);
byte b = (byte)Convert.ToUInt32(hex.Substring(4, 2), 16);
return new SolidColorBrush(Color.FromArgb(255, r, g, b));
}
}

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Extension;
public static class EnumerableExtensions
{
/// <summary>
/// 将源转换为仅包含单个元素的集合
/// 将源转换为仅包含单个元素的枚举
/// </summary>
/// <typeparam name="TSource">源的类型</typeparam>
/// <param name="source">源</param>
@@ -79,51 +79,6 @@ public static class EnumerableExtensions
return source.FirstOrDefault(predicate) ?? source.FirstOrDefault();
}
/// <summary>
/// 将二维可枚举对象一维化
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>扁平的对象</returns>
public static IEnumerable<TSource> Flatten<TSource>(this IEnumerable<IEnumerable<TSource>> source)
{
return source.SelectMany(x => x);
}
/// <summary>
/// 对集合中的每个物品执行指定的操作
/// </summary>
/// <typeparam name="TSource">集合类型</typeparam>
/// <param name="source">集合</param>
/// <param name="action">指定的操作</param>
/// <returns>修改后的集合</returns>
public static IEnumerable<TSource> ForEach<TSource>(this IEnumerable<TSource> source, Action<TSource> action)
{
foreach (TSource item in source)
{
action(item);
}
return source;
}
/// <summary>
/// 对集合中的每个物品执行指定的操作
/// </summary>
/// <typeparam name="TSource">集合类型</typeparam>
/// <param name="source">集合</param>
/// <param name="func">指定的操作</param>
/// <returns>修改后的集合</returns>
public static async Task<IEnumerable<TSource>> ForEachAsync<TSource>(this IEnumerable<TSource> source, Func<TSource, Task> func)
{
foreach (TSource item in source)
{
await func(item);
}
return source;
}
/// <summary>
/// 表示一个对 <see cref="TItem"/> 类型的计数器
/// </summary>

View File

@@ -8,7 +8,7 @@ using WinRT.Interop;
namespace Snap.Hutao.Factory;
/// <inheritdoc cref="IPickerFactory"/>
[Injection(InjectAs.Transient)]
[Injection(InjectAs.Transient, typeof(IPickerFactory))]
internal class PickerFactory : IPickerFactory
{
private readonly MainWindow mainWindow;

View File

@@ -42,7 +42,6 @@ internal static class IocConfiguration
public static IServiceCollection AddDatebase(this IServiceCollection services)
{
MyDocumentContext myDocument = new(new());
myDocument.EnsureDirectory();
string dbFile = myDocument.Locate("Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";

View File

@@ -66,12 +66,6 @@ public class User : Observable
private set => Set(ref selectedUserGameRole, value);
}
/// <summary>
/// 移除命令
/// </summary>
[NotMapped]
public ICommand? RemoveCommand { get; set; }
/// <summary>
/// 复制Cookie命令
/// </summary>
@@ -126,7 +120,6 @@ public class User : Observable
}
CopyCookieCommand = new RelayCommand(CopyCookie);
Must.NotNull(RemoveCommand!);
UserInfo = await userClient
.GetUserFullInfoAsync(this, token)

View File

@@ -18,5 +18,5 @@ public class DescParam
/// <summary>
/// 参数
/// </summary>
public IEnumerable<LevelParam<int>> Parameters { get; set; } = default!;
public IEnumerable<LevelParam<int, double>> Parameters { get; set; } = default!;
}

View File

@@ -40,26 +40,27 @@ public class FetterInfo
/// </summary>
public int BirthDay { get; set; }
/// <summary>
/// 格式化的生日日期
/// </summary>
public string BirthFormatted
{
get
{
return $"{BirthMonth} 月 {BirthDay} 日";
}
}
/// <summary>
/// 神之眼属性-前
/// </summary>
public string VisionBefore { get; set; } = default!;
/// <summary>
/// 神之眼属性-后
/// </summary>
public string VisionAfter { get; set; } = default!;
/// <summary>
/// 命座-前
/// </summary>
public string ConstellationBefore { get; set; } = default!;
/// <summary>
/// 命座-后
/// </summary>
public string ConstellationAfter { get; set; } = default!;
/// <summary>
/// 中文CV
/// </summary>

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色头像转换器
/// </summary>
internal class IconConverter : IValueConverter
internal class AvatarIconConverter : IValueConverter
{
private const string BaseUrl = "https://upload-bbs.mihoyo.com/game_record/genshin/character_icon/{0}.png";

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色侧面头像转换器
/// </summary>
internal class SideIconConverter : IValueConverter
internal class AvatarSideIconConverter : IValueConverter
{
private const string BaseUrl = "https://upload-bbs.mihoyo.com/game_record/genshin/character_side_icon/{0}.png";

View File

@@ -0,0 +1,115 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Model.Metadata.Avatar;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 描述参数解析器
/// </summary>
internal class DescParamDescriptor : IValueConverter
{
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
DescParam descParam = (DescParam)value;
IEnumerable<DescFormat> parsedDescriptions = descParam.Descriptions.Select(desc =>
{
string[] parts = desc.Split('|', 2);
return new DescFormat(parts[0], parts[1]);
});
IList<IList<string>> parameters = descParam.Parameters
.Select(param =>
{
IList<string> parameters = GetFormattedParameters(parsedDescriptions, param.Parameters);
parameters.Insert(0, param.Level.ToString());
return parameters;
})
.ToList();
List<string> descList = parsedDescriptions.Select(p => p.Description).ToList();
descList.Insert(0, "等级");
return new DescParamInternal(descList, parameters);
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
private IList<string> GetFormattedParameters(IEnumerable<DescFormat> formats, IList<double> param)
{
List<string> results = new();
foreach (DescFormat descFormat in formats)
{
string format = descFormat.Format;
string resultFormatted = Regex.Replace(format, @"{param\d+.*?}", match => EvaluateMatch(match, param));
results.Add(resultFormatted);
}
return results;
}
private string EvaluateMatch(Match match, IList<double> param)
{
if (match.Success)
{
string[] parts = match.Value[1..^1].Split(':', 2);
int index = int.Parse(parts[0][5..]) - 1;
if (parts[1] == "I")
{
return ((int)param[index]).ToString();
}
if (parts[1] == "F1P")
{
return string.Format("{0:P1}", param[index]);
}
if (parts[1] == "F2P")
{
return string.Format("{0:P2}", param[index]);
}
return string.Format($"{{0:{parts[1]}}}", param[index]);
}
else
{
return string.Empty;
}
}
private class DescFormat
{
public DescFormat(string description, string format)
{
Description = description;
Format = format;
}
public string Description { get; set; }
public string Format { get; set; }
}
private class DescParamInternal
{
public DescParamInternal(IList<string> descriptions, IList<IList<string>> parameters)
{
Descriptions = descriptions;
Parameters = parameters;
}
public IList<string> Descriptions { get; set; }
public IList<IList<string>> Parameters { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 元素名称图标转换器
/// </summary>
internal class ElementNameIconConverter : IValueConverter
{
private const string BaseUrl = "https://static.snapgenshin.com/IconElement/UI_Icon_Element_{0}.png";
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
string element = (string)value switch
{
"雷" => "Electric",
"火" => "Fire",
"草" => "Grass",
"冰" => "Ice",
"岩" => "Rock",
"水" => "Water",
"风" => "Wind",
_ => throw Must.NeverHappen(),
};
return new Uri(string.Format(BaseUrl, element));
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
}

View File

@@ -30,4 +30,4 @@ internal class QualityConverter : IValueConverter
{
throw Must.NeverHappen();
}
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 元素名称图标转换器
/// </summary>
internal class WeaponTypeIconConverter : IValueConverter
{
private const string BaseUrl = "https://static.snapgenshin.com/Skill/Skill_A_{0}.png";
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
string element = (WeaponType)value switch
{
WeaponType.WEAPON_SWORD_ONE_HAND => "01",
WeaponType.WEAPON_BOW => "02",
WeaponType.WEAPON_POLE => "03",
WeaponType.WEAPON_CLAYMORE => "04",
WeaponType.WEAPON_CATALYST => "Catalyst_MD",
_ => throw Must.NeverHappen(),
};
return new Uri(string.Format(BaseUrl, element));
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
}

View File

@@ -9,7 +9,8 @@ namespace Snap.Hutao.Model.Metadata;
/// 等级与参数
/// </summary>
/// <typeparam name="TLevel">等级的类型</typeparam>
public class LevelParam<TLevel>
/// <typeparam name="TParam">参数的类型</typeparam>
public class LevelParam<TLevel, TParam>
{
/// <summary>
/// 等级
@@ -19,5 +20,5 @@ public class LevelParam<TLevel>
/// <summary>
/// 参数
/// </summary>
public IList<double> Parameters { get; set; } = default!;
public IList<TParam> Parameters { get; set; } = default!;
}

View File

@@ -19,5 +19,5 @@ public class PropertyInfo
/// <summary>
/// 参数
/// </summary>
public IEnumerable<LevelParam<string>> Parameters { get; set; } = default!;
public IEnumerable<LevelParam<string, double>> Parameters { get; set; } = default!;
}

View File

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

View File

@@ -24,7 +24,7 @@ public static class Program
Application.Start(p =>
{
DispatcherQueueSynchronizationContext context = new(DispatcherQueue.GetForCurrentThread());
SynchronizationContext context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
_ = new App();
});

View File

@@ -69,11 +69,4 @@ internal interface IMetadataService
/// <param name="token">取消令牌</param>
/// <returns>武器列表</returns>
ValueTask<IEnumerable<Weapon>> GetWeaponsAsync(CancellationToken token = default);
/// <summary>
/// 异步更新元数据
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>更新是否完成</returns>
Task<bool> UpdateMetadataAsync(CancellationToken token = default);
}

View File

@@ -25,6 +25,7 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
{
private const string MetaAPIHost = "http://hutao-metadata.snapgenshin.com";
private const string MetaFileName = "Meta.json";
private readonly IInfoBarService infoBarService;
private readonly HttpClient httpClient;
private readonly FileSystemContext metadataContext;
@@ -78,40 +79,14 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
public async Task InitializeInternalAsync(CancellationToken token = default)
{
logger.LogInformation("元数据初始化开始");
metadataContext.EnsureDirectory();
IsInitialized = await UpdateMetadataAsync(token)
IsInitialized = await TryUpdateMetadataAsync(token)
.ConfigureAwait(false);
initializeCompletionSource.SetResult();
logger.LogInformation("元数据初始化完成");
}
/// <inheritdoc/>
public async Task<bool> UpdateMetadataAsync(CancellationToken token = default)
{
IDictionary<string, string>? metaMd5Map = await httpClient
.GetFromJsonAsync<IDictionary<string, string>>($"{MetaAPIHost}/{MetaFileName}", options, token)
.ConfigureAwait(false);
if (metaMd5Map is null)
{
infoBarService.Error("元数据校验文件解析失败");
return false;
}
await CheckMetadataAsync(metaMd5Map, token).ConfigureAwait(false);
using (FileStream metaFileStream = metadataContext.Create(MetaFileName))
{
await JsonSerializer
.SerializeAsync(metaFileStream, metaMd5Map, options, token)
.ConfigureAwait(false);
}
return true;
}
/// <inheritdoc/>
public ValueTask<IEnumerable<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default)
{
@@ -154,34 +129,30 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
return GetMetadataAsync<IEnumerable<Weapon>>("Weapon", token);
}
private async ValueTask<T> GetMetadataAsync<T>(string fileName, CancellationToken token)
where T : class
private async Task<bool> TryUpdateMetadataAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败");
string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}";
if (memoryCache.TryGetValue(cacheKey, out object? value))
{
return Must.NotNull((value as T)!);
}
T? result = await JsonSerializer
.DeserializeAsync<T>(metadataContext.OpenRead($"{fileName}.json"), options, token)
// download meta check file
IDictionary<string, string>? metaMd5Map = await httpClient
.GetFromJsonAsync<IDictionary<string, string>>($"{MetaAPIHost}/{MetaFileName}", options, token)
.ConfigureAwait(false);
return memoryCache.Set(cacheKey, Must.NotNull(result!));
}
private async Task<string> GetFileMd5Async(string fileFullName, CancellationToken token)
{
using (FileStream stream = metadataContext.OpenRead(fileFullName))
if (metaMd5Map is null)
{
byte[] bytes = await MD5.Create()
.ComputeHashAsync(stream, token)
.ConfigureAwait(false);
return Convert.ToHexString(bytes);
infoBarService.Error("元数据校验文件解析失败");
return false;
}
await CheckMetadataAsync(metaMd5Map, token).ConfigureAwait(false);
// save metadataFile
using (FileStream metaFileStream = metadataContext.Create(MetaFileName))
{
await JsonSerializer
.SerializeAsync(metaFileStream, metaMd5Map, options, token)
.ConfigureAwait(false);
}
return true;
}
/// <summary>
@@ -193,6 +164,7 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
/// <returns>令牌</returns>
private async Task CheckMetadataAsync(IDictionary<string, string> metaMd5Map, CancellationToken token)
{
// TODO: Make this foreach async to imporve speed
// enumerate files and compare md5
foreach ((string fileName, string md5) in metaMd5Map)
{
@@ -215,6 +187,18 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
}
}
private async Task<string> GetFileMd5Async(string fileFullName, CancellationToken token)
{
using (FileStream stream = metadataContext.OpenRead(fileFullName))
{
byte[] bytes = await MD5.Create()
.ComputeHashAsync(stream, token)
.ConfigureAwait(false);
return Convert.ToHexString(bytes);
}
}
private async Task DownloadMetadataAsync(string fileFullName, CancellationToken token)
{
Stream sourceStream = await httpClient
@@ -238,4 +222,22 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
logger.LogInformation("{file} 下载完成", fileFullName);
}
private async ValueTask<T> GetMetadataAsync<T>(string fileName, CancellationToken token)
where T : class
{
Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败");
string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}";
if (memoryCache.TryGetValue(cacheKey, out object? value))
{
return Must.NotNull((value as T)!);
}
T? result = await JsonSerializer
.DeserializeAsync<T>(metadataContext.OpenRead($"{fileName}.json"), options, token)
.ConfigureAwait(false);
return memoryCache.Set(cacheKey, Must.NotNull(result!));
}
}

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Service.Navigation;
/// <summary>
/// 为 <see cref="NavigationExtra"/> 提供抽象接口
/// </summary>
public interface INavigationExtra
public interface INavigationData
{
/// <summary>
/// 数据

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Service.Navigation;
/// <summary>
/// 导航额外信息
/// </summary>
public class NavigationExtra : INavigationExtra, INavigationAwaiter
public class NavigationExtra : INavigationData, INavigationAwaiter
{
/// <summary>
/// 任务完成源

View File

@@ -7,6 +7,7 @@ using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.View.Helper;
using Snap.Hutao.View.Page;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.Service.Navigation;
@@ -85,8 +86,7 @@ internal class NavigationService : INavigationService
}
else
{
NavigationViewItem? target = NavigationView.MenuItems
.OfType<NavigationViewItem>()
NavigationViewItem? target = EnumerateMenuItems(NavigationView.MenuItems)
.SingleOrDefault(menuItem => NavHelper.GetNavigateTo(menuItem) == pageType);
NavigationView.SelectedItem = target;
@@ -163,6 +163,24 @@ internal class NavigationService : INavigationService
NavigationView.IsPaneOpen = LocalSetting.GetValueType(SettingKeys.IsNavPaneOpen, true);
}
/// <summary>
/// 遍历所有子菜单项
/// </summary>
/// <param name="items">项列表</param>
/// <returns>枚举器</returns>
private IEnumerable<NavigationViewItem> EnumerateMenuItems(IList<object> items)
{
foreach (NavigationViewItem item in items.OfType<NavigationViewItem>())
{
yield return item;
foreach (NavigationViewItem subItem in EnumerateMenuItems(item.MenuItems))
{
yield return subItem;
}
}
}
private void OnItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args)
{
Selected = NavigationView?.SelectedItem as NavigationViewItem;
@@ -170,8 +188,12 @@ internal class NavigationService : INavigationService
? typeof(SettingPage)
: NavHelper.GetNavigateTo(Selected);
INavigationAwaiter navigationAwaiter = new NavigationExtra(NavHelper.GetExtraData(Selected));
Navigate(Must.NotNull(targetType!), navigationAwaiter, false);
// ignore item that doesn't have nav type specified
if (targetType != null)
{
INavigationAwaiter navigationAwaiter = new NavigationExtra(NavHelper.GetExtraData(Selected));
Navigate(targetType, navigationAwaiter, false);
}
}
private void OnBackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args)

View File

@@ -24,7 +24,7 @@ public interface IUserService
/// </summary>
/// <param name="removeCommand">移除用户命令</param>
/// <returns>准备完成的用户信息枚举</returns>
Task<ObservableCollection<User>> GetInitializedUsersAsync(ICommand removeCommand);
Task<ObservableCollection<User>> GetInitializedUsersAsync();
/// <summary>
/// 异步添加用户

View File

@@ -121,7 +121,7 @@ internal class UserService : IUserService
}
/// <inheritdoc/>
public async Task<ObservableCollection<User>> GetInitializedUsersAsync(ICommand removeCommand)
public async Task<ObservableCollection<User>> GetInitializedUsersAsync()
{
if (cachedUsers == null)
{
@@ -133,7 +133,6 @@ internal class UserService : IUserService
foreach (User user in cachedUsers)
{
user.RemoveCommand = removeCommand;
await user
.InitializeAsync(userClient, userGameRoleClient)
.ConfigureAwait(false);

View File

@@ -41,7 +41,6 @@
<None Remove="View\Page\AnnouncementContentPage.xaml" />
<None Remove="View\Page\AnnouncementPage.xaml" />
<None Remove="View\Page\SettingPage.xaml" />
<None Remove="View\Page\WelcomePage.xaml" />
<None Remove="View\Page\WikiAvatarPage.xaml" />
<None Remove="View\TitleView.xaml" />
<None Remove="View\UserView.xaml" />
@@ -154,11 +153,6 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\WelcomePage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\UserDialog.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -8,10 +8,10 @@
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
mc:Ignorable="d"
Width="64"
Height="64">
Width="80"
Height="80">
<UserControl.Resources>
<shmmc:IconConverter x:Key="IconConverter"/>
<shmmc:AvatarIconConverter x:Key="IconConverter"/>
<shmmc:QualityConverter x:Key="QualityConverter"/>
</UserControl.Resources>
<Grid>

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.View.Helper;
public sealed class NavHelper
{
private static readonly DependencyProperty NavigateToProperty = Property<NavHelper>.Attach<Type>("NavigateTo");
private static readonly DependencyProperty ExtraDataProperty = Property<NavHelper>.Attach<INavigationExtra>("ExtraData");
private static readonly DependencyProperty ExtraDataProperty = Property<NavHelper>.Attach<INavigationData>("ExtraData");
/// <summary>
/// 获取导航项的目标页面类型
@@ -41,9 +41,9 @@ public sealed class NavHelper
/// </summary>
/// <param name="item">待获取的导航项</param>
/// <returns>目标页面类型的额外数据</returns>
public static INavigationExtra? GetExtraData(NavigationViewItem? item)
public static INavigationData? GetExtraData(NavigationViewItem? item)
{
return item?.GetValue(ExtraDataProperty) as INavigationExtra;
return item?.GetValue(ExtraDataProperty) as INavigationData;
}
/// <summary>
@@ -51,7 +51,7 @@ public sealed class NavHelper
/// </summary>
/// <param name="item">待设置的导航项</param>
/// <param name="value">新的目标页面类型</param>
public static void SetExtraData(NavigationViewItem item, INavigationExtra value)
public static void SetExtraData(NavigationViewItem item, INavigationData value)
{
item.SetValue(ExtraDataProperty, value);
}

View File

@@ -12,13 +12,12 @@
<Thickness x:Key="NavigationViewContentMargin">0,48,0,0</Thickness>
</UserControl.Resources>
<Grid>
<!-- x:Bind can't get property update here seems like a WinUI 3 bug-->
<NavigationView
x:Name="NavView"
CompactPaneLength="48"
OpenPaneLength="244"
CompactModeThresholdWidth="128"
ExpandedModeThresholdWidth="720"
CompactModeThresholdWidth="16"
ExpandedModeThresholdWidth="16"
IsPaneOpen="True"
IsBackEnabled="{Binding ElementName=ContentFrame,Path=CanGoBack}">
@@ -26,7 +25,8 @@
<NavigationViewItem Content="活动" helper:NavHelper.NavigateTo="page:AnnouncementPage">
<NavigationViewItem.Icon>
<BitmapIcon UriSource="ms-appx:///Resource/Icon/UI_BtnIcon_ActivityEntry.png"/>
<BitmapIcon
UriSource="ms-appx:///Resource/Icon/UI_BtnIcon_ActivityEntry.png"/>
</NavigationViewItem.Icon>
</NavigationViewItem>

View File

@@ -1,7 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Control.Cancellable;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View.Page;
@@ -19,4 +21,15 @@ public sealed partial class AchievementPage : CancellablePage
InitializeWith<AchievementViewModel>();
InitializeComponent();
}
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData extra)
{
extra.NotifyNavigationCompleted();
}
}
}

View File

@@ -36,14 +36,14 @@ openInWebview: function(url){ location.href = url }}";
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationExtra extra)
if (e.Parameter is INavigationData extra)
{
targetContent = extra.Data as string;
LoadAnnouncementAsync(extra).SafeForget();
}
}
private async Task LoadAnnouncementAsync(INavigationExtra extra)
private async Task LoadAnnouncementAsync(INavigationData extra)
{
try
{

View File

@@ -27,9 +27,7 @@
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
</shcc:CancellablePage.Resources>
<Grid>
<ScrollViewer
Padding="0,0,4,0"
Visibility="{Binding OpeningUI.IsWorking,Converter={StaticResource BoolToVisibilityRevertConverter}}">
<ScrollViewer Padding="0,0,4,0">
<ItemsControl
HorizontalAlignment="Stretch"
ItemsSource="{Binding Announcement.List}"

View File

@@ -1,7 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Control.Cancellable;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View.Page;
@@ -19,4 +21,15 @@ public sealed partial class AnnouncementPage : CancellablePage
InitializeWith<AnnouncementViewModel>();
InitializeComponent();
}
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData extra)
{
extra.NotifyNavigationCompleted();
}
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View.Page;
@@ -18,4 +20,15 @@ public sealed partial class SettingPage : Microsoft.UI.Xaml.Controls.Page
DataContext = Ioc.Default.GetRequiredService<SettingViewModel>();
InitializeComponent();
}
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData extra)
{
extra.NotifyNavigationCompleted();
}
}
}

View File

@@ -1,16 +0,0 @@
<Page
x:Class="Snap.Hutao.View.Page.WelcomePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Snap.Hutao.View.Page"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid Padding="12">
<TextBlock
Text="欢迎使用胡桃"
Style="{StaticResource TitleTextBlockStyle}"/>
</Grid>
</Page>

View File

@@ -1,18 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.View.Page;
/// <summary>
/// 欢迎页
/// </summary>
public sealed partial class WelcomePage : Microsoft.UI.Xaml.Controls.Page
{
/// <summary>
/// 构造一个新的欢迎页
/// </summary>
public WelcomePage()
{
InitializeComponent();
}
}

View File

@@ -8,6 +8,8 @@
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shct="using:Snap.Hutao.Control.Text"
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
xmlns:shvc="using:Snap.Hutao.View.Control"
xmlns:shv="using:Snap.Hutao.ViewModel"
@@ -20,42 +22,166 @@
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
<Page.Resources>
<shmmc:IconConverter x:Key="IconConverter"/>
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
<shmmc:FightPropertyConverter x:Key="FightPropertyConverter"/>
<shmmc:FightPropertyValueFormatter x:Key="FightPropertyValueFormatter"/>
<shmmc:DescParamDescriptor x:Key="DescParamDescriptor"/>
<shc:BindingProxy
x:Key="FightPropertyBindingProxy"
DataContext="{Binding Selected.Property.Properties}"/>
<SolidColorBrush
x:Key="SystemControlPageBackgroundChromeLowBrush"
Color="{StaticResource SystemChromeLowColor}"
Opacity="0.2"/>
<DataTemplate x:Key="SkillDataTemplate">
<Expander
Margin="16,16,0,0"
Header="{Binding Name}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<shct:DescriptionTextBlock
Margin="16,16,16,0"
Description="{Binding Description}">
<shct:DescriptionTextBlock.Resources>
<Style
TargetType="TextBlock"
BasedOn="{StaticResource BodyTextBlockStyle}">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</shct:DescriptionTextBlock.Resources>
</shct:DescriptionTextBlock>
<ScrollViewer
Grid.Row="1"
VerticalScrollMode="Disabled"
HorizontalScrollBarVisibility="Auto">
<Grid
Margin="16"
DataContext="{Binding Proud,Converter={StaticResource DescParamDescriptor}}">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ItemsControl
Grid.Row="0"
ItemsSource="{Binding Descriptions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid
ColumnSpacing="16"
Columns="{Binding Descriptions.Count}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding}"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
Style="{StaticResource BaseTextBlockStyle}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl
Margin="0,16,0,0"
Grid.Row="2"
ItemsSource="{Binding Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid
ColumnSpacing="16"
Columns="{Binding Count}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding}"
TextTrimming="CharacterEllipsis"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
</Grid>
</Expander>
</DataTemplate>
<DataTemplate x:Key="InherentDataTemplate">
<Expander
Margin="16,16,0,0"
Header="{Binding Name}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<shct:DescriptionTextBlock
Margin="16"
Description="{Binding Description}">
<shct:DescriptionTextBlock.Resources>
<Style
TargetType="TextBlock"
BasedOn="{StaticResource BodyTextBlockStyle}">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</shct:DescriptionTextBlock.Resources>
</shct:DescriptionTextBlock>
</Expander>
</DataTemplate>
</Page.Resources>
<Grid>
<SplitView
IsPaneOpen="True"
DisplayMode="Inline"
OpenPaneLength="374">
OpenPaneLength="200">
<SplitView.PaneBackground>
<SolidColorBrush Color="{StaticResource CardBackgroundFillColorSecondary}"/>
</SplitView.PaneBackground>
<SplitView.Pane>
<GridView
<ListView
SelectionMode="Single"
ItemsSource="{Binding Avatars}"
Padding="16,16,0,12"
SelectedItem="{Binding Selected,Mode=TwoWay}">
<GridView.ItemTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<shvc:ItemIcon
Quality="{Binding Quality,Mode=OneWay}"
Icon="{Binding Icon,Converter={StaticResource IconConverter},Mode=OneWay}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shci:CachedImage
Grid.Column="0"
Width="48"
Height="48"
Margin="0,0,12,12"
Source="{Binding SideIcon,Converter={StaticResource AvatarSideIconConverter},Mode=OneWay}"/>
<TextBlock
VerticalAlignment="Center"
Grid.Column="1"
Margin="12,0,0,0"
Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</ListView.ItemTemplate>
</ListView>
</SplitView.Pane>
<SplitView.Content>
<ScrollViewer>
<StackPanel Margin="0,0,16,16">
<!--头图-->
<!--简介-->
<Grid
Margin="16,16,0,16"
VerticalAlignment="Top">
@@ -63,23 +189,130 @@
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shvc:ItemIcon
Grid.Column="0"
Height="100"
Width="100"
Quality="{Binding Selected.Quality,Mode=OneWay}"
Icon="{Binding Selected.Icon,Converter={StaticResource IconConverter},Mode=OneWay}"/>
<StackPanel Grid.Column="0">
<Grid Margin="0,0,0,16" >
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shci:CachedImage
Grid.Column="0"
Width="27.2"
Height="27.2"
Source="{Binding Selected.FetterInfo.VisionBefore,Converter={StaticResource ElementNameIconConverter}}"/>
<shci:CachedImage
Grid.Column="1"
Width="27.2"
Height="27.2"
Source="{Binding Selected.Weapon,Converter={StaticResource WeaponTypeIconConverter}}"/>
</Grid>
<shvc:ItemIcon
Height="100"
Width="100"
Quality="{Binding Selected.Quality,Mode=OneWay}"
Icon="{Binding Selected.Icon,Converter={StaticResource AvatarIconConverter},Mode=OneWay}"/>
</StackPanel>
<StackPanel
Margin="16,0,0,0"
Grid.Column="1">
<StackPanel Orientation="Horizontal">
<TextBlock
Text="{Binding Selected.Name}"
Style="{StaticResource SubtitleTextBlockStyle}"/>
<TextBlock
Margin="24,0,0,0"
Text="{Binding Selected.FetterInfo.Title}"
Style="{StaticResource SubtitleTextBlockStyle}"/>
</StackPanel>
<TextBlock
Text="{Binding Selected.Name}"
Style="{StaticResource SubtitleTextBlockStyle}"/>
<TextBlock
Margin="0,6,0,0"
Text="{Binding Selected.Description}"
MaxLines="2"
Margin="0,12,0,0"
Text="{Binding Selected.FetterInfo.Detail}"
TextWrapping="NoWrap"
Style="{StaticResource CaptionTextBlockStyle}"/>
<cwuc:UniformGrid
Columns="4"
ColumnSpacing="12"
Margin="0,12,0,0">
<StackPanel>
<TextBlock
Text="所属"
Style="{StaticResource BaseTextBlockStyle}"/>
<TextBlock
Margin="0,6,0,0"
TextWrapping="NoWrap"
Text="{Binding Selected.FetterInfo.Native}"
Style="{StaticResource CaptionTextBlockStyle}"/>
</StackPanel>
<StackPanel>
<TextBlock
Text="命之座"
Style="{StaticResource BaseTextBlockStyle}"/>
<TextBlock
Margin="0,6,0,0"
TextWrapping="NoWrap"
Text="{Binding Selected.FetterInfo.ConstellationBefore}"
Style="{StaticResource CaptionTextBlockStyle}"/>
</StackPanel>
<StackPanel>
<TextBlock
Text="生日"
Style="{StaticResource BaseTextBlockStyle}"/>
<TextBlock
Margin="0,6,0,0"
TextWrapping="NoWrap"
Text="{Binding Selected.FetterInfo.BirthFormatted}"
Style="{StaticResource CaptionTextBlockStyle}"/>
</StackPanel>
</cwuc:UniformGrid>
<cwuc:UniformGrid
Margin="0,12,0,0"
ColumnSpacing="12"
Columns="4">
<StackPanel>
<TextBlock
Text="汉语 CV"
Style="{StaticResource BaseTextBlockStyle}"/>
<TextBlock
Margin="0,6,0,0"
TextWrapping="NoWrap"
Text="{Binding Selected.FetterInfo.CvChinese}"
Style="{StaticResource CaptionTextBlockStyle}"/>
</StackPanel>
<StackPanel>
<TextBlock
Text="日语 CV"
Style="{StaticResource BaseTextBlockStyle}"/>
<TextBlock
Margin="0,6,0,0"
TextWrapping="NoWrap"
Text="{Binding Selected.FetterInfo.CvJapanese}"
Style="{StaticResource CaptionTextBlockStyle}"/>
</StackPanel>
<StackPanel>
<TextBlock
Text="英语 CV"
Style="{StaticResource BaseTextBlockStyle}"/>
<TextBlock
Margin="0,6,0,0"
TextWrapping="NoWrap"
Text="{Binding Selected.FetterInfo.CvEnglish}"
Style="{StaticResource CaptionTextBlockStyle}"/>
</StackPanel>
<StackPanel>
<TextBlock
Text="韩语 CV"
Style="{StaticResource BaseTextBlockStyle}"/>
<TextBlock
Margin="0,6,0,0"
TextWrapping="NoWrap"
Text="{Binding Selected.FetterInfo.CvKorean}"
Style="{StaticResource CaptionTextBlockStyle}"/>
</StackPanel>
</cwuc:UniformGrid>
</StackPanel>
</Grid>
<!--属性-->
@@ -88,75 +321,209 @@
Header="基础数值"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<Grid Margin="16">
<ScrollViewer
VerticalScrollMode="Disabled"
HorizontalScrollBarVisibility="Auto">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<cwuc:UniformGrid
Grid.Row="0"
Columns="5"
ColumnSpacing="16"
DataContext="{Binding Selected.Property}">
<TextBlock
Style="{StaticResource BaseTextBlockStyle}"
TextWrapping="NoWrap"
Text="等级"/>
<TextBlock
TextWrapping="NoWrap"
Style="{StaticResource BaseTextBlockStyle}"
Text="{Binding Properties[0], Converter={StaticResource FightPropertyConverter}}"/>
<TextBlock
TextWrapping="NoWrap"
Style="{StaticResource BaseTextBlockStyle}"
Text="{Binding Properties[1], Converter={StaticResource FightPropertyConverter}}"/>
<TextBlock
TextWrapping="NoWrap"
Style="{StaticResource BaseTextBlockStyle}"
Text="{Binding Properties[2], Converter={StaticResource FightPropertyConverter}}"/>
<TextBlock
TextWrapping="NoWrap"
Style="{StaticResource BaseTextBlockStyle}"
Text="{Binding Properties[3], Converter={StaticResource FightPropertyConverter}}"/>
</cwuc:UniformGrid>
<ItemsControl
Margin="0,16,0,0"
Grid.Row="1"
ItemsSource="{Binding Selected.Property.Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<cwuc:UniformGrid
Columns="5"
ColumnSpacing="16">
<TextBlock
Text="{Binding Level}"/>
<TextBlock
Text="{Binding Parameters[0],
Converter={StaticResource FightPropertyValueFormatter},
ConverterParameter={Binding DataContext[0],Source={StaticResource FightPropertyBindingProxy}}}"/>
<TextBlock
Text="{Binding Parameters[1],
Converter={StaticResource FightPropertyValueFormatter},
ConverterParameter={Binding DataContext[1],Source={StaticResource FightPropertyBindingProxy}}}"/>
<TextBlock
Text="{Binding Parameters[2],
Converter={StaticResource FightPropertyValueFormatter},
ConverterParameter={Binding DataContext[2],Source={StaticResource FightPropertyBindingProxy}}}"/>
<TextBlock
Text="{Binding Parameters[3],
Converter={StaticResource FightPropertyValueFormatter},
ConverterParameter={Binding DataContext[3],Source={StaticResource FightPropertyBindingProxy}}}"/>
</cwuc:UniformGrid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
</Expander>
<TextBlock Text="天赋" Style="{StaticResource BaseTextBlockStyle}" Margin="16,16,0,0"/>
<ItemsControl
ItemsSource="{Binding Selected.SkillDepot.Skills}"
ItemTemplate="{StaticResource SkillDataTemplate}"/>
<!--元素爆发-->
<Expander
Margin="16,16,0,0"
Header="{Binding Name}"
DataContext="{Binding Selected.SkillDepot.EnergySkill}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<cwuc:UniformGrid
Grid.Row="0"
Columns="5"
DataContext="{Binding Selected.Property}"
x:Name="PropertyGrid">
<TextBlock
Style="{StaticResource BaseTextBlockStyle}"
TextWrapping="NoWrap"
Text="等级"/>
<TextBlock
TextWrapping="NoWrap"
Style="{StaticResource BaseTextBlockStyle}"
Text="{Binding Properties[0], Converter={StaticResource FightPropertyConverter}}"/>
<TextBlock
TextWrapping="NoWrap"
Style="{StaticResource BaseTextBlockStyle}"
Text="{Binding Properties[1], Converter={StaticResource FightPropertyConverter}}"/>
<TextBlock
TextWrapping="NoWrap"
Style="{StaticResource BaseTextBlockStyle}"
Text="{Binding Properties[2], Converter={StaticResource FightPropertyConverter}}"/>
<TextBlock
TextWrapping="NoWrap"
Style="{StaticResource BaseTextBlockStyle}"
Text="{Binding Properties[3], Converter={StaticResource FightPropertyConverter}}"/>
</cwuc:UniformGrid>
<ItemsControl
Margin="0,6,0,0"
<shct:DescriptionTextBlock
Margin="16,16,16,0"
Description="{Binding Description}">
<shct:DescriptionTextBlock.Resources>
<Style
TargetType="TextBlock"
BasedOn="{StaticResource BodyTextBlockStyle}">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</shct:DescriptionTextBlock.Resources>
</shct:DescriptionTextBlock>
<ScrollViewer
Grid.Row="1"
ItemsSource="{Binding Selected.Property.Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<cwuc:UniformGrid
Columns="5">
<TextBlock
Text="{Binding Level}"/>
<TextBlock
Text="{Binding Parameters[0],
Converter={StaticResource FightPropertyValueFormatter},
ConverterParameter={Binding DataContext[0],Source={StaticResource FightPropertyBindingProxy}}}"/>
<TextBlock
Text="{Binding Parameters[1],
Converter={StaticResource FightPropertyValueFormatter},
ConverterParameter={Binding DataContext[1],Source={StaticResource FightPropertyBindingProxy}}}"/>
<TextBlock
Text="{Binding Parameters[2],
Converter={StaticResource FightPropertyValueFormatter},
ConverterParameter={Binding DataContext[2],Source={StaticResource FightPropertyBindingProxy}}}"/>
<TextBlock
Text="{Binding Parameters[3],
Converter={StaticResource FightPropertyValueFormatter},
ConverterParameter={Binding DataContext[3],Source={StaticResource FightPropertyBindingProxy}}}"/>
</cwuc:UniformGrid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
VerticalScrollMode="Disabled"
HorizontalScrollBarVisibility="Auto">
<Grid>
<Grid
Grid.Row="1"
Margin="16"
DataContext="{Binding Proud,Converter={StaticResource DescParamDescriptor}}">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ItemsControl
Grid.Row="0"
ItemsSource="{Binding Descriptions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid
ColumnSpacing="16"
Columns="{Binding Descriptions.Count}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding}"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
Style="{StaticResource BaseTextBlockStyle}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl
Margin="0,6,0,0"
Grid.Row="2"
ItemsSource="{Binding Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid
ColumnSpacing="16"
Columns="{Binding Count}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding}"
TextTrimming="CharacterEllipsis"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Grid>
</ScrollViewer>
</Grid>
</Expander>
<ItemsControl
ItemsSource="{Binding Selected.SkillDepot.Inherents}"
ItemTemplate="{StaticResource InherentDataTemplate}"/>
<TextBlock Text="命之座" Style="{StaticResource BaseTextBlockStyle}" Margin="16,16,0,0"/>
<ItemsControl
ItemsSource="{Binding Selected.SkillDepot.Talents}"
ItemTemplate="{StaticResource InherentDataTemplate}"/>
<TextBlock Text="其他" Style="{StaticResource BaseTextBlockStyle}" Margin="16,16,0,0"/>
<!--衣装-->
<Expander
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Header="衣装">
<ItemsControl
ItemsSource="{Binding Selected.Costumes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,-4">
<TextBlock
Margin="16,16,16,0"
Text="{Binding Name}"/>
<TextBlock
Margin="16,8,16,16"
Text="{Binding Description}"
Style="{StaticResource CaptionTextBlockStyle}"/>
<MenuFlyoutSeparator Margin="16,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
<!--资料-->
<Expander
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Header="语音">
Header="资料">
<ItemsControl
ItemsSource="{Binding Selected.FetterInfo.Fetters}">
<ItemsControl.ItemTemplate>
@@ -165,10 +532,15 @@
<TextBlock
Margin="16,16,16,0"
Text="{Binding Title}"/>
<TextBlock
<shct:DescriptionTextBlock
Margin="16,8,16,16"
Text="{Binding Context}"
Style="{StaticResource CaptionTextBlockStyle}"/>
Description="{Binding Context}">
<shct:DescriptionTextBlock.Resources>
<Style
TargetType="TextBlock"
BasedOn="{StaticResource CaptionTextBlockStyle}"/>
</shct:DescriptionTextBlock.Resources>
</shct:DescriptionTextBlock>
<MenuFlyoutSeparator Margin="16,0"/>
</StackPanel>
</DataTemplate>
@@ -204,4 +576,4 @@
</SplitView.Content>
</SplitView>
</Grid>
</Page>
</Page>

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View.Page;
@@ -18,4 +20,15 @@ public sealed partial class WikiAvatarPage : Microsoft.UI.Xaml.Controls.Page
DataContext = Ioc.Default.GetRequiredService<WikiAvatarViewModel>();
InitializeComponent();
}
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData extra)
{
extra.NotifyNavigationCompleted();
}
}
}

View File

@@ -1,12 +1,13 @@
<UserControl
x:Class="Snap.Hutao.View.UserView"
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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
xmlns:mxim="using:Microsoft.Xaml.Interactions.Media"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shc="using:Snap.Hutao.Control"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Loaded">
@@ -26,8 +27,9 @@
<PersonPicture
ProfilePicture="{Binding SelectedUser.UserInfo.AvatarUrl,Mode=OneWay}"
HorizontalAlignment="Left"
Margin="4,0,4,0"
Height="40"/>
Margin="6,2"
Height="36"
Width="36"/>
<TextBlock
VerticalAlignment="Center"
Margin="0,0,0,2"
@@ -42,6 +44,11 @@
FontFamily="{StaticResource SymbolThemeFontFamily}"
Grid.Column="2"
Margin="4">
<Button.Resources>
<shc:BindingProxy
x:Key="ViewModelBindingProxy"
DataContext="{Binding}"/>
</Button.Resources>
<Button.Flyout>
<Flyout
Placement="TopEdgeAlignedRight"
@@ -124,7 +131,8 @@
BorderThickness="0"
BorderBrush="{x:Null}"
Margin="12,0,0,0"
Command="{Binding CopyCookieCommand}"
Command="{Binding DataContext.CopyCookieCommand,Source={StaticResource ViewModelBindingProxy}}"
CommandParameter="{Binding}"
ToolTipService.ToolTip="复制 Cookie"/>
<Button
Content="&#xE74D;"
@@ -135,7 +143,7 @@
BorderThickness="0"
BorderBrush="{x:Null}"
Margin="6,0,0,0"
Command="{Binding RemoveCommand}"
Command="{Binding DataContext.RemoveUserCommand,Source={StaticResource ViewModelBindingProxy}}"
CommandParameter="{Binding}"
ToolTipService.ToolTip="移除用户"/>
</StackPanel>

View File

@@ -5,7 +5,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Control.Cancellable;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Navigation;
@@ -64,11 +63,6 @@ internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
set => SetProperty(ref announcement, value);
}
/// <summary>
/// 打开界面监视器
/// </summary>
public Watcher OpeningUI { get; } = new(false);
/// <summary>
/// 打开界面触发的命令
/// </summary>
@@ -81,23 +75,18 @@ internal class AnnouncementViewModel : ObservableObject, ISupportCancellation
private async Task OpenUIAsync()
{
using (OpeningUI.Watch())
try
{
try
{
Announcement = await announcementService.GetAnnouncementsAsync(OpenAnnouncementUICommand, CancellationToken);
}
catch (TaskCanceledException)
{
logger.LogInformation($"{nameof(OpenUIAsync)} cancelled");
}
Announcement = await announcementService.GetAnnouncementsAsync(OpenAnnouncementUICommand, CancellationToken);
}
catch (TaskCanceledException)
{
logger.LogInformation($"{nameof(OpenUIAsync)} cancelled");
}
}
private void OpenAnnouncementUI(string? content)
{
logger.LogInformation($"{nameof(OpenAnnouncementUICommand)} Triggered");
if (WebView2Helper.IsSupported)
{
navigationService.Navigate<AnnouncementContentPage>(data: new NavigationExtra(content));

View File

@@ -12,7 +12,6 @@ internal class SettingViewModel
/// <summary>
/// 构造一个新的测试视图模型
/// </summary>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public SettingViewModel()
{
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Entity;
@@ -10,6 +11,7 @@ using Snap.Hutao.View.Dialog;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Windows.ApplicationModel.DataTransfer;
namespace Snap.Hutao.ViewModel;
@@ -23,7 +25,6 @@ internal class UserViewModel : ObservableObject
private readonly IUserService userService;
private readonly IInfoBarService infoBarService;
private readonly ICommand removeUserCommandCache;
private User? selectedUser;
private ObservableCollection<User>? userInfos;
@@ -41,8 +42,8 @@ internal class UserViewModel : ObservableObject
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
AddUserCommand = asyncRelayCommandFactory.Create(AddUserAsync);
removeUserCommandCache = asyncRelayCommandFactory.Create<User>(RemoveUserAsync);
RemoveUserCommand = asyncRelayCommandFactory.Create<User>(RemoveUserAsync);
CopyCookieCommand = new RelayCommand<User>(CopyCookie);
}
/// <summary>
@@ -75,6 +76,16 @@ internal class UserViewModel : ObservableObject
/// </summary>
public ICommand AddUserCommand { get; }
/// <summary>
/// 移除用户命令
/// </summary>
public ICommand RemoveUserCommand { get; }
/// <summary>
/// 复制Cookie命令
/// </summary>
public ICommand CopyCookieCommand { get; }
private static bool TryValidateCookie(IDictionary<string, string> map, [NotNullWhen(true)] out IDictionary<string, string>? filteredCookie)
{
int validFlag = 4;
@@ -104,7 +115,7 @@ internal class UserViewModel : ObservableObject
private async Task OpenUIAsync()
{
Users = await userService.GetInitializedUsersAsync(removeUserCommandCache);
Users = await userService.GetInitializedUsersAsync();
SelectedUser = userService.CurrentUser;
}
@@ -121,11 +132,7 @@ internal class UserViewModel : ObservableObject
if (TryValidateCookie(cookieMap, out IDictionary<string, string>? filteredCookie))
{
string simplifiedCookie = string.Join(';', filteredCookie.Select(kvp => $"{kvp.Key}={kvp.Value}"));
User user = new()
{
Cookie = simplifiedCookie,
RemoveCommand = removeUserCommandCache,
};
User user = new() { Cookie = simplifiedCookie };
switch (await userService.TryAddUserAsync(user, filteredCookie[AccountIdKey]))
{
@@ -160,4 +167,26 @@ internal class UserViewModel : ObservableObject
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 成功移除");
}
}
private void CopyCookie(User? user)
{
if (User.IsNone(user))
{
return;
}
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
try
{
DataPackage content = new();
content.SetText(Must.NotNull(user.Cookie!));
Clipboard.SetContent(content);
infoBarService.Success($"{user.UserInfo!.Nickname} 的 Cookie 复制成功");
}
catch (Exception e)
{
infoBarService.Error(e);
}
}
}

View File

@@ -11,7 +11,6 @@ using Snap.Hutao.Web.Hutao.Model;
using Snap.Hutao.Web.Hutao.Model.Post;
using Snap.Hutao.Web.Response;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
@@ -325,46 +324,6 @@ internal class HutaoClient : ISupportAsyncInitialization
return await response.Content.ReadFromJsonAsync<Response<string>>(jsonSerializerOptions, token);
}
/// <summary>
/// 异步上传物品所有物品
/// </summary>
/// <param name="characters">角色详细信息</param>
/// <param name="token">取消令牌</param>
/// <returns>响应</returns>
[Obsolete("不再强制要求上传物品")]
[EditorBrowsable(EditorBrowsableState.Never)]
internal async Task<Response<string>?> UploadItemsAsync(List<Character> characters, CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
IEnumerable<Item> avatars = characters
.Select(avatar => new Item(avatar.Id, avatar.Name, avatar.Icon))
.DistinctBy(item => item.Id);
IEnumerable<Item> weapons = characters
.Select(avatar => avatar.Weapon)
.Select(weapon => new Item(weapon.Id, weapon.Name, weapon.Icon))
.DistinctBy(item => item.Id);
IEnumerable<Item> reliquaries = characters
.Select(avatars => avatars.Reliquaries)
.Flatten()
.Where(relic => relic.Position == ReliquaryPosition.FlowerOfLife)
.DistinctBy(relic => relic.Id)
.Select(relic => new Item(relic.ReliquarySet.Id, relic.ReliquarySet.Name, relic.Icon));
GenshinItemWrapper? data = new(avatars, weapons, reliquaries);
JsonSerializerOptions? option = Ioc.Default.GetService<JsonSerializerOptions>();
HttpResponseMessage? response = await httpClient
.PostAsJsonAsync($"{HutaoAPI}/GenshinItem/Upload", data, jsonSerializerOptions, token)
.ConfigureAwait(false);
return await response.Content
.ReadFromJsonAsync<Response<string>>(jsonSerializerOptions, token)
.ConfigureAwait(false);
}
private class Auth
{
public Auth(string appid, string secret)