Merge pull request #1131 from DGP-Studio/develop

This commit is contained in:
DismissedLight
2023-11-27 19:34:55 +08:00
committed by GitHub
71 changed files with 2197 additions and 680 deletions

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
@@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" Version="3.3.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public class CollectionsMarshalTest
{
[TestMethod]
public void DictionaryMarshalGetValueRefOrNullRefIsNullRef()
{
#if NET8_0_OR_GREATER
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
Dictionary<string, string> dictionaryRefKeyRefValue = [];
#else
Dictionary<uint, string> dictionaryValueKeyRefValue = new();
Dictionary<uint, uint> dictionaryValueKeyValueValue = new();
Dictionary<string, uint> dictionaryRefKeyValueValue = new();
Dictionary<string, string> dictionaryRefKeyRefValue = new();
#endif
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyRefValue, 1U)));
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyValueValue, 1U)));
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyValueValue, "no such key")));
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyRefValue, "no such key")));
}
[TestMethod]
public void DictionaryMarshalGetValueRefOrAddDefaultIsDefault()
{
#if NET8_0_OR_GREATER
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
Dictionary<string, string> dictionaryRefKeyRefValue = [];
#else
Dictionary<uint, string> dictionaryValueKeyRefValue = new();
Dictionary<uint, uint> dictionaryValueKeyValueValue = new();
Dictionary<string, uint> dictionaryRefKeyValueValue = new();
Dictionary<string, string> dictionaryRefKeyRefValue = new();
#endif
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyRefValue, 1U, out _) == default);
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyValueValue, 1U, out _) == default);
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyValueValue, "no such key", out _) == default);
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyRefValue, "no such key", out _) == default);
}
}

View File

@@ -7,9 +7,7 @@ namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public class JsonSerializeTest
{
private TestContext? testContext;
public TestContext? TestContext { get => testContext; set => testContext = value; }
public TestContext? TestContext { get; set; }
private readonly JsonSerializerOptions AlowStringNumberOptions = new()
{

View File

@@ -0,0 +1,217 @@
using System;
using System.Buffers.Binary;
using System.Globalization;
using System.Text;
namespace Snap.Hutao.Test.IncomingFeature;
[TestClass]
public sealed class GeniusInvokationDecoding
{
public TestContext? TestContext { get; set; }
/// <summary>
/// https://www.bilibili.com/video/av278125720
/// </summary>
[TestMethod]
public unsafe void GeniusInvokationShareCodeDecoding()
{
// 51 bytes obfuscated data
byte[] bytes = Convert.FromBase64String("BCHBwxQNAYERyVANCJGBynkOCZER2pgOCrFx8poQChGR9bYQDEGB9rkQDFKRD7oRDeEB");
// ---------------------------------------------
// | Data | Caesar Cipher Key |
// |----------|-------------------|
// | 50 Bytes | 1 Byte |
// ---------------------------------------------
// Data:
// 00000100 00100001 11000001 11000011 00010100
// 00001101 00000001 10000001 00010001 11001001
// 01010000 00001101 00001000 10010001 10000001
// 11001010 01111001 00001110 00001001 10010001
// 00010001 11011010 10011000 00001110 00001010
// 10110001 01110001 11110010 10011010 00010000
// 00001010 00010001 10010001 11110101 10110110
// 00010000 00001100 01000001 10000001 11110110
// 10111001 00010000 00001100 01010010 10010001
// 00001111 10111010 00010001 00001101 11100001
// ---------------------------------------------
// Caesar Cipher Key:
// 00000001
// ---------------------------------------------
fixed (byte* ptr = bytes)
{
// Reinterpret as 50 byte actual data and 1 deobfuscate key byte
EncryptedDataAndKey* data = (EncryptedDataAndKey*)ptr;
byte* dataPtr = data->Data;
// ----------------------------------------------------------
// | First | Second | Padding |
// |-----------|----------|---------|
// | 25 Bytes | 25 Bytes | 1 Byte |
// ----------------------------------------------------------
// We are doing two things here:
// 1. Retrieve actual data by subtracting key
// 2. Store data into two halves by alternating between them
// ----------------------------------------------------------
// What we will get after this step:
// ----------------------------------------------------------
// First:
// 00000011 11000000 00010011 00000000 00010000
// 01001111 00000111 10000000 01111000 00001000
// 00010000 10010111 00001001 01110000 10011001
// 00001001 10010000 10110101 00001011 10000000
// 10111000 00001011 10010000 10111001 00001100
// ----------------------------------------------------------
// Second:
// 00100000 11000010 00001100 10000000 11001000
// 00001100 10010000 11001001 00001101 10010000
// 11011001 00001101 10110000 11110001 00001111
// 00010000 11110100 00001111 01000000 11110101
// 00001111 01010001 00001110 00010000 11100000
// ----------------------------------------------------------
RearrangeBuffer rearranged = default;
byte* pFirst = rearranged.First;
byte* pSecond = rearranged.Second;
for (int i = 0; i < 50; i++)
{
// Determine which half are we going to insert
byte** ppTarget = i % 2 == 0 ? &pFirst : &pSecond;
// (actual data = data - key) and store it directly to the target half
**ppTarget = unchecked((byte)(dataPtr[i] - data->Key));
(*ppTarget)++;
}
// Prepare decoded data result storage
DecryptedData decoded = default;
ushort* pDecoded = decoded.Data;
// ----------------------------------------------------------
// | Data |
// |----------| x 17 = 51 Bytes
// | 3 Bytes |
// ----------------------------------------------------------
// Grouping each 3 bytes and read out as 2 ushort with
// 12 bits each (Big Endian)
// ----------------------------------------------------------
// 00000011 1100·0000 00010011|
// 00000000 0001·0000 01001111|
// 00000111 1000·0000 01111000|
// 00001000 0001·0000 10010111|
// 00001001 0111·0000 10011001|
// 00001001 1001·0000 10110101|
// 00001011 1000·0000 10111000|
// 00001011 1001·0000 10111001|
// 00001100 0010·0000 11000010|
// 00001100 1000·0000 11001000|
// 00001100 1001·0000 11001001|
// 00001101 1001·0000 11011001|
// 00001101 1011·0000 11110001|
// 00001111 0001·0000 11110100|
// 00001111 0100·0000 11110101|
// 00001111 0101·0001 00001110|
// 00010000 1110·0000 -padding|[padding32]
// ----------------------------------------------------------
// reinterpret as DecodeGroupingHelper for each 3 bytes
DecodeGroupingHelper* pGroup = (DecodeGroupingHelper*)&rearranged;
for (int i = 0; i < 17; i++)
{
(ushort first, ushort second) = pGroup->GetData();
*pDecoded = first;
*(pDecoded + 1) = second;
pDecoded += 2;
pGroup++;
}
// Now we get
// 60, 19, 1,
// 79,120,120,
// 129,151,151,
// 153,153,181,
// 184,184,185,
// 185,194,194,
// 200,200,201,
// 201,217,217,
// 219,241,241,
// 244,244,245,
// 245,270,270,
StringBuilder stringBuilder = new();
for (int i = 0; i < 33; i++)
{
stringBuilder
.AppendFormat(CultureInfo.InvariantCulture, "{0,3}", decoded.Data[i])
.Append(',');
if (i % 11 == 10)
{
stringBuilder.Append('\n');
}
}
TestContext?.WriteLine(stringBuilder.ToString(0, stringBuilder.Length - 1));
ushort[] resultArray = new ushort[33];
Span<ushort> result = new((ushort*)&decoded, 33);
result.CopyTo(resultArray);
ushort[] testKnownResult =
#if NET8_0_OR_GREATER
[
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
];
#else
{
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
};
#endif
CollectionAssert.AreEqual(resultArray, testKnownResult);
}
}
private struct EncryptedDataAndKey
{
public unsafe fixed byte Data[50];
public byte Key;
}
private struct RearrangeBuffer
{
public unsafe fixed byte First[25];
public unsafe fixed byte Second[25];
// Make it 51 bytes
// allow to be group as 17 DecodeGroupingHelper later
public byte padding;
// prevent accidently int32 cast access violation
public byte paddingTo32;
}
private struct DecodeGroupingHelper
{
public unsafe fixed byte Data[3];
public unsafe (ushort First, ushort Second) GetData()
{
fixed (byte* ptr = Data)
{
uint value = BinaryPrimitives.ReverseEndianness((*(uint*)ptr) & 0x00FFFFFF) >> 8; // keep low 24 bits only
return ((ushort)((value >> 12) & 0x0FFF), (ushort)(value & 0x0FFF));
}
}
}
private struct DecryptedData
{
public unsafe fixed ushort Data[33];
}
}

View File

@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -4,12 +4,18 @@
"add": {
"extensionToExtension": {
"add": {
".json": [ ".txt" ]
".json": [
".txt"
]
}
},
"pathSegment": {
"add": {
".*": [ ".cs", ".resx" ]
".*": [
".cs",
".resx",
".appxmanifest"
]
}
},
"fileSuffixToExtension": {
@@ -19,12 +25,24 @@
},
"fileToFile": {
"add": {
".filenesting.json": [ "App.xaml.cs" ],
"app.manifest": [ "App.xaml.cs" ],
"Package.appxmanifest": [ "App.xaml" ],
"Package.StoreAssociation.xml": [ "App.xaml" ],
".editorconfig": [ "Program.cs" ],
"GlobalUsing.cs": [ "Program.cs" ]
".filenesting.json": [
"App.xaml.cs"
],
"app.manifest": [
"App.xaml.cs"
],
"Package.appxmanifest": [
"App.xaml"
],
"Package.StoreAssociation.xml": [
"App.xaml"
],
".editorconfig": [
"Program.cs"
],
"GlobalUsing.cs": [
"Program.cs"
]
}
}
}

View File

@@ -10,6 +10,7 @@
<ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Card.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/ComboBox.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>

View File

@@ -30,7 +30,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
private readonly IServiceProvider serviceProvider;
private readonly RoutedEventHandler unloadEventHandler;
private readonly SizeChangedEventHandler sizeChangedEventHandler;
private readonly TypedEventHandler<LoadedImageSurface, LoadedImageSourceLoadCompletedEventArgs> loadedImageSourceLoadCompletedEventHandler;
@@ -46,9 +45,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
serviceProvider = this.ServiceProvider();
this.DisableInteraction();
unloadEventHandler = OnUnload;
Unloaded += unloadEventHandler;
sizeChangedEventHandler = OnSizeChanged;
SizeChanged += sizeChangedEventHandler;
@@ -67,10 +63,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{
}
protected virtual void Unloading()
{
}
/// <summary>
/// 更新视觉对象
/// </summary>
@@ -240,14 +232,4 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
UpdateVisual(spriteVisual);
}
}
private void OnUnload(object sender, RoutedEventArgs e)
{
Unloading();
spriteVisual?.Dispose();
spriteVisual = null;
SizeChanged -= sizeChangedEventHandler;
Unloaded -= unloadEventHandler;
}
}

View File

@@ -45,16 +45,6 @@ internal sealed class MonoChrome : CompositionImage
return compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
}
protected override void Unloading()
{
ActualThemeChanged -= actualThemeChangedEventHandler;
backgroundBrush?.Dispose();
backgroundBrush = null;
base.Unloading();
}
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
if (backgroundBrush is not null)

View File

@@ -128,15 +128,8 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
UniformStaggeredItem item = state.GetItemAt(i);
if (item.Height == 0)
{
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
// The first element must be force refreshed otherwise
// it will use the old one realized
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Now we need to refresh the first element of each column
ElementRealizationOptions options = i < numberOfColumns ? ElementRealizationOptions.ForceCreate : ElementRealizationOptions.None;
// Item has not been measured yet. Get the element and store the values
UIElement element = context.GetOrCreateElementAt(i, options);
UIElement element = context.GetOrCreateElementAt(i);
element.Measure(new Size(state.ColumnWidth, availableHeight));
item.Height = element.DesiredSize.Height;
item.Element = element;
@@ -209,11 +202,8 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
{
UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex);
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
for (int i = 0; i < layoutSpan.Length; i++)
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(layout))
{
ref readonly UniformStaggeredItem item = ref layoutSpan[i];
double bottom = item.Top + item.Height;
if (bottom < context.RealizationRect.Top)
{

View File

@@ -62,11 +62,9 @@ internal sealed class UniformStaggeredLayoutState
}
}
[SuppressMessage("", "SH007")]
internal UniformStaggeredColumnLayout GetColumnLayout(int columnIndex)
{
this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout);
return columnLayout!;
return columnLayout[columnIndex];
}
/// <summary>
@@ -74,6 +72,21 @@ internal sealed class UniformStaggeredLayoutState
/// </summary>
internal void Clear()
{
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
// The first element must be force refreshed otherwise
// it will use the old one realized
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Now we need to refresh the first element of each column
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Finally we need to refresh the whole layout when we reset
if (context.ItemCount > 0)
{
for (int i = 0; i < context.ItemCount; i++)
{
RecycleElementAt(i);
}
}
columnLayout.Clear();
items.Clear();
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Windows.Foundation;
namespace Snap.Hutao.Control;
[DependencyProperty("IsWidthRestricted", typeof(bool), true)]
[DependencyProperty("IsHeightRestricted", typeof(bool), true)]
internal sealed partial class SizeRestrictedContentControl : ContentControl
{
private double minContentWidth;
private double minContentHeight;
protected override Size MeasureOverride(Size availableSize)
{
if (Content is FrameworkElement element)
{
element.Measure(availableSize);
Size contentDesiredSize = element.DesiredSize;
Size contentActualOrDesiredSize = new(
Math.Max(element.ActualWidth, contentDesiredSize.Width),
Math.Max(element.ActualHeight, contentDesiredSize.Height));
if (IsWidthRestricted)
{
if (contentActualOrDesiredSize.Width > minContentWidth)
{
minContentWidth = contentActualOrDesiredSize.Width;
}
element.MinWidth = minContentWidth;
}
if (IsHeightRestricted)
{
if (contentActualOrDesiredSize.Height > minContentHeight)
{
minContentHeight = contentActualOrDesiredSize.Height;
}
element.MinHeight = minContentHeight;
}
}
return base.MeasureOverride(availableSize);
}
}

View File

@@ -0,0 +1,11 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style
x:Key="CommandBarComboBoxStyle"
BasedOn="{StaticResource DefaultComboBoxStyle}"
TargetType="ComboBox">
<Setter Property="Padding" Value="12,7,0,7"/>
<Setter Property="Height" Value="36"/>
</Style>
<!-- https://github.com/microsoft/microsoft-ui-xaml/issues/4811 -->
<x:Int32 x:Key="__DiscardPageOverride">0</x:Int32>
</ResourceDictionary>

View File

@@ -93,4 +93,19 @@
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<SolidColorBrush x:Key="TextControlTextForeground" Color="{ThemeResource TextFillColorPrimary}"/>
<Thickness x:Key="TextControlTextMargin">1,0</Thickness>
<Style
x:Key="TextControlTextBlockStyle"
BasedOn="{StaticResource BodyTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextControlTextForeground}"/>
<Setter Property="Margin" Value="{StaticResource TextControlTextMargin}"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalTextAlignment" Value="Center"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
</ResourceDictionary>

View File

@@ -17,6 +17,9 @@
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing2Template">
<StackPanel Orientation="Horizontal" Spacing="2"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
<StackPanel Spacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
<cwcont:UniformGrid
ColumnSpacing="2"

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
internal interface IPinnable<TData>
{
ref readonly TData GetPinnableReference();
}

View File

@@ -63,8 +63,10 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.CNVersion);
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
client.DefaultRequestHeaders.Add("x-rpc-device_name", string.Empty);
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
//client.DefaultRequestHeaders.Add("x-rpc-tool_verison", "v4.2.2-ys");
}
/// <summary>

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Discord;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Navigation;
@@ -165,6 +166,8 @@ internal sealed partial class Activation : IActivation
serviceProvider.GetRequiredService<MainWindow>();
await taskContext.SwitchToBackgroundAsync();
serviceProvider
.GetRequiredService<IMetadataService>()
.As<IMetadataServiceInitialization>()?
@@ -176,6 +179,11 @@ internal sealed partial class Activation : IActivation
.As<IHutaoUserServiceInitialization>()?
.InitializeInternalAsync()
.SafeForget();
serviceProvider
.GetRequiredService<IDiscordService>()
.SetNormalActivity()
.SafeForget();
}
}

View File

@@ -34,6 +34,8 @@ internal sealed class RuntimeOptions : IOptions<RuntimeOptions>
{
this.logger = logger;
AppLaunchTime = DateTimeOffset.UtcNow;
DataFolder = GetDataFolderPath();
LocalCache = ApplicationData.Current.LocalCacheFolder.Path;
InstalledLocation = Package.Current.InstalledLocation.Path;
@@ -96,6 +98,8 @@ internal sealed class RuntimeOptions : IOptions<RuntimeOptions>
/// </summary>
public bool IsElevated { get => isElevated ??= GetElevated(); }
public DateTimeOffset AppLaunchTime { get; }
/// <inheritdoc/>
public RuntimeOptions Value { get => this; }

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Extension;
/// <see cref="StringBuilder"/> 扩展方法
/// </summary>
[HighQuality]
internal static class StringBuilderExtensions
internal static class StringBuilderExtension
{
/// <summary>
/// 当条件符合时执行 <see cref="StringBuilder.Append(string?)"/>
@@ -37,4 +37,21 @@ internal static class StringBuilderExtensions
{
return condition ? sb.Append(value) : sb;
}
public static string ToStringTrimEndReturn(this StringBuilder builder)
{
Must.Argument(builder.Length >= 1, "StringBuilder 的长度必须大于 0");
int remove = 0;
if (builder[^1] is '\n')
{
remove = 1;
if (builder.Length >= 2 && builder[^2] is '\r')
{
remove = 2;
}
}
return builder.ToString(0, builder.Length - remove);
}
}

View File

@@ -10,10 +10,19 @@ internal static class WinRTExtension
{
public static bool IsDisposed(this IWinRTObject obj)
{
return GetDisposed(obj.NativeObject);
IObjectReference objectReference = obj.NativeObject;
lock (GetDisposedLock(objectReference))
{
return GetDisposed(objectReference);
}
}
// protected bool disposed;
[UnsafeAccessor(UnsafeAccessorKind.Field, Name ="disposed")]
private static extern ref bool GetDisposed(IObjectReference objRef);
// private object _disposedLock
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposedLock")]
private static extern ref object GetDisposedLock(IObjectReference objRef);
}

View File

@@ -16,8 +16,8 @@ namespace Snap.Hutao;
[SuppressMessage("", "CA1001")]
internal sealed partial class MainWindow : Window, IWindowOptionsSource, IMinMaxInfoHandler
{
private const int MinWidth = 1200;
private const int MinHeight = 750;
private const int MinWidth = 1000;
private const int MinHeight = 600;
private readonly WindowOptions windowOptions;
private readonly ILogger<MainWindow> logger;

View File

@@ -0,0 +1,555 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20231126113631_AddUserLastUpdateTime")]
partial class AddUserLastUpdateTime
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<uint>("Current")
.HasColumnType("INTEGER");
b.Property<uint>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CalculatorRefreshTime")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("GameRecordRefreshTime")
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ShowcaseRefreshTime")
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("Id")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("cultivate_entries");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("Count")
.HasColumnType("INTEGER");
b.Property<Guid>("EntryId")
.HasColumnType("TEXT");
b.Property<bool>("IsFinished")
.HasColumnType("INTEGER");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("EntryId");
b.ToTable("cultivate_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachedUid")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("cultivate_projects");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DailyNote")
.HasColumnType("TEXT");
b.Property<bool>("DailyTaskNotify")
.HasColumnType("INTEGER");
b.Property<bool>("DailyTaskNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotify")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("HomeCoinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("HomeCoinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RefreshTime")
.HasColumnType("TEXT");
b.Property<bool>("ResinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("ResinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotify")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("daily_notes");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("GachaType")
.HasColumnType("INTEGER");
b.Property<long>("Id")
.HasColumnType("INTEGER");
b.Property<uint>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("QueryType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("Count")
.HasColumnType("INTEGER");
b.Property<uint>("ItemId")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("inventory_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AppendPropIdList")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("Level")
.HasColumnType("INTEGER");
b.Property<int>("MainPropId")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("inventory_reliquaries");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("Level")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.Property<int>("PromoteLevel")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("inventory_weapons");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpireTime")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("object_cache");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("ScheduleId")
.HasColumnType("INTEGER");
b.Property<string>("SpiralAbyss")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("spiral_abysses");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Aid")
.HasColumnType("TEXT");
b.Property<string>("CookieToken")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CookieTokenLastUpdateTime")
.HasColumnType("TEXT");
b.Property<string>("Fingerprint")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("FingerprintLastUpdateTime")
.HasColumnType("TEXT");
b.Property<bool>("IsOversea")
.HasColumnType("INTEGER");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("LToken")
.HasColumnType("TEXT")
.HasColumnName("Ltoken");
b.Property<string>("Mid")
.HasColumnType("TEXT");
b.Property<string>("SToken")
.HasColumnType("TEXT")
.HasColumnName("Stoken");
b.HasKey("InnerId");
b.ToTable("users");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
.WithMany()
.HasForeignKey("EntryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entry");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,42 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
/// <inheritdoc />
public partial class AddUserLastUpdateTime : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "CookieTokenLastUpdateTime",
table: "users",
type: "TEXT",
nullable: false,
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
migrationBuilder.AddColumn<DateTimeOffset>(
name: "FingerprintLastUpdateTime",
table: "users",
type: "TEXT",
nullable: false,
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CookieTokenLastUpdateTime",
table: "users");
migrationBuilder.DropColumn(
name: "FingerprintLastUpdateTime",
table: "users");
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.13");
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
@@ -428,9 +428,15 @@ namespace Snap.Hutao.Migrations
b.Property<string>("CookieToken")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CookieTokenLastUpdateTime")
.HasColumnType("TEXT");
b.Property<string>("Fingerprint")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("FingerprintLastUpdateTime")
.HasColumnType("TEXT");
b.Property<bool>("IsOversea")
.HasColumnType("INTEGER");

View File

@@ -106,6 +106,8 @@ internal sealed partial class SettingEntry
public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics";
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";
/// <summary>
/// 启动游戏 多倍启动
/// </summary>

View File

@@ -65,6 +65,10 @@ internal sealed class User : ISelectable, IMappingFrom<User, Cookie, bool>
/// </summary>
public string? Fingerprint { get; set; }
public DateTimeOffset FingerprintLastUpdateTime { get; set; }
public DateTimeOffset CookieTokenLastUpdateTime { get; set; }
/// <summary>
/// 创建一个新的用户
/// </summary>

View File

@@ -14,32 +14,32 @@ internal static class IntrinsicFrozen
/// <summary>
/// 所属地区
/// </summary>
public static readonly FrozenSet<string> AssociationTypes = Enum.GetValues<AssociationType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
public static FrozenSet<string> AssociationTypes { get; } = Enum.GetValues<AssociationType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
/// <summary>
/// 武器类型
/// </summary>
public static readonly FrozenSet<string> WeaponTypes = Enum.GetValues<WeaponType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
public static FrozenSet<string> WeaponTypes { get; } = Enum.GetValues<WeaponType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
/// <summary>
/// 物品类型
/// </summary>
public static readonly FrozenSet<string> ItemQualities = Enum.GetValues<QualityType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
public static FrozenSet<string> ItemQualities { get; } = Enum.GetValues<QualityType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
/// <summary>
/// 身材类型
/// </summary>
public static readonly FrozenSet<string> BodyTypes = Enum.GetValues<BodyType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
public static FrozenSet<string> BodyTypes { get; } = Enum.GetValues<BodyType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
/// <summary>
/// 战斗属性
/// </summary>
public static readonly FrozenSet<string> FightProperties = Enum.GetValues<FightProperty>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
public static FrozenSet<string> FightProperties { get; } = Enum.GetValues<FightProperty>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToFrozenSet();
/// <summary>
/// 元素名称
/// </summary>
public static readonly FrozenSet<string> ElementNames = FrozenSet.ToFrozenSet(
public static FrozenSet<string> ElementNames { get; } = FrozenSet.ToFrozenSet(
[
SH.ModelIntrinsicElementNameFire,
SH.ModelIntrinsicElementNameWater,
@@ -50,7 +50,7 @@ internal static class IntrinsicFrozen
SH.ModelIntrinsicElementNameRock,
]);
public static readonly FrozenSet<string> MaterialTypeDescriptions = FrozenSet.ToFrozenSet(
public static FrozenSet<string> MaterialTypeDescriptions { get; } = FrozenSet.ToFrozenSet(
[
SH.ModelMetadataMaterialCharacterAndWeaponEnhancementMaterial,
SH.ModelMetadataMaterialCharacterEXPMaterial,

View File

@@ -14,8 +14,5 @@
<ReservedName>胡桃工具箱</ReservedName>
</ReservedNames>
</ProductReservedInfo>
<AccountPackageIdentityNames>
<MainPackageIdentityName>60568DGPStudio.SnapGenshinResin</MainPackageIdentityName>
</AccountPackageIdentityNames>
<PackageInfoList LandingUrl="https://developer.microsoft.com/dashboard/Application?appId=9PH4NXJ2JN52" />
</StoreAssociation>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
IgnorableNamespaces="com uap desktop rescap mp">
<Identity
Name="60568DGPStudio.SnapHutaoDev"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.8.0.0" />
<Properties>
<DisplayName>Snap Hutao Dev</DisplayName>
<PublisherDisplayName>DGP Studio</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>
<Resource Language="en-us"/>
<Resource Language="zh-cn"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Snap Hutao Dev"
Description="A Snap Hutao Dev Software"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" Square71x71Logo="Assets\SmallTile.png" Square310x310Logo="Assets\LargeTile.png"/>
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="5760ec4d-f7e8-4666-a965-9886d7dffe7d"/>
</desktop:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Snap.Hutao.exe" Arguments="-ToastActivated" DisplayName="Snap Hutao Dev Toast Activator">
<com:Class Id="5760ec4d-f7e8-4666-a965-9886d7dffe7d" DisplayName="Snap Hutao Dev Toast Activator"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="hutao">
<uap:DisplayName>胡桃 Dev</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
</Capabilities>
</Package>

View File

@@ -948,7 +948,7 @@
<value>Unable to find cached metadata file</value>
</data>
<data name="ServiceMetadataHttpRequestFailed" xml:space="preserve">
<value>HTTP {0} | Error:{1}:元数据校验文件下载失败</value>
<value>HTTP {0} | Error {1}: Failed to download metadata verification file</value>
</data>
<data name="ServiceMetadataNotInitialized" xml:space="preserve">
<value>Metadata service has not been initialized or failed to initialize</value>
@@ -2012,6 +2012,9 @@
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>Screen Resolution</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioPlaceHolder" xml:space="preserve">
<value>Resolution Quick Set</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>Create window as popup, without frame</value>
</data>
@@ -2057,6 +2060,12 @@
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
<value>All options will be saved only after the game is launched successfully.</value>
</data>
<data name="ViewPageLaunchGameDiscordActivityDescription" xml:space="preserve">
<value>Set My Discord Activity Status When I'm in the Game</value>
</data>
<data name="ViewPageLaunchGameDiscordActivityHeader" xml:space="preserve">
<value>Discord Activity</value>
</data>
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>File</value>
</data>
@@ -2597,6 +2606,9 @@
<data name="ViewUserDocumentationHeader" xml:space="preserve">
<value>Document</value>
</data>
<data name="ViewUserNoUserHint" xml:space="preserve">
<value>No Login</value>
</data>
<data name="ViewUserRefreshCookieTokenSuccess" xml:space="preserve">
<value>Refresh CookieToken successfully</value>
</data>
@@ -2757,7 +2769,7 @@
<value>Wrong UID format</value>
</data>
<data name="WebEnkaResponseStatusCode404" xml:space="preserve">
<value>Role UID does not exist</value>
<value>Role UID does not exist, please try again later</value>
</data>
<data name="WebEnkaResponseStatusCode424" xml:space="preserve">
<value>Game in maintenance</value>

View File

@@ -130,7 +130,7 @@
<value>キャンセル</value>
</data>
<data name="ContentDialogCompletePrimaryButtonText" xml:space="preserve">
<value>完了</value>
<value>OK</value>
</data>
<data name="ContentDialogConfirmPrimaryButtonText" xml:space="preserve">
<value>確定</value>
@@ -600,10 +600,10 @@
<value>{0} つのアチーブメントを追加 | {1} つのアチーブメントを更新 |{2} つのアチーブメントを削除</value>
</data>
<data name="ServiceAchievementUIAFImportPickerFilterText" xml:space="preserve">
<value>UIAF Json 文件</value>
<value>UIAF Json ファイル</value>
</data>
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>打开 UIAF Json 文件</value>
<value>UIAF Json ファイルを開く</value>
</data>
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>複数の同一アチーブメント Idがアーカイブに混在しています</value>
@@ -792,10 +792,10 @@
<value>参量物質変化器は使用可能</value>
</data>
<data name="ServiceDiscordGameActivityDetails" xml:space="preserve">
<value>正在提瓦特大陆中探索</value>
<value>テイワット大陸を探索</value>
</data>
<data name="ServiceDiscordGameLaunchedBy" xml:space="preserve">
<value> {0} 启动</value>
<value>スタートから {0}</value>
</data>
<data name="ServiceGachaLogArchiveCollectionUserdataCorruptedMessage" xml:space="preserve">
<value>{0}:祈願履歴を確認できません</value>
@@ -882,7 +882,7 @@
<value>ゲーム本体を選択する</value>
</data>
<data name="ServiceGameLocatorPickerFilterText" xml:space="preserve">
<value>游戏本体</value>
<value>ゲームクライアント</value>
</data>
<data name="ServiceGameLocatorUnityLogFileNotFound" xml:space="preserve">
<value>Unity ログファイルが見つかりません</value>
@@ -948,7 +948,7 @@
<value>キャッシュされたメタデータファイルが見つかりませんでした</value>
</data>
<data name="ServiceMetadataHttpRequestFailed" xml:space="preserve">
<value>HTTP {0} | Error:{1}:元数据校验文件下载失败</value>
<value>HTTP {0} | Error {1}: メタデータ検証ファイルのダウンロードに失敗しました。</value>
</data>
<data name="ServiceMetadataNotInitialized" xml:space="preserve">
<value>メタデータサービスが初期化されていないか、または初期化できませんでした</value>
@@ -1350,7 +1350,7 @@
<value>{0} を削除してもよろしいでしょうか?</value>
</data>
<data name="ViewModelAchievementUIAFExportPickerTitle" xml:space="preserve">
<value>导出 UIAF Json 文件到指定路径</value>
<value>UIAF Json ファイルを指定した場所へエクスポート</value>
</data>
<data name="ViewModelAvatarPropertyBatchCultivateProgressTitle" xml:space="preserve">
<value>素材リスト取得中、しばらくお待ちください</value>
@@ -1479,13 +1479,13 @@
<value>胡桃クラウドで祈願履歴を同期します</value>
</data>
<data name="ViewModelGachaLogUIGFExportPickerTitle" xml:space="preserve">
<value>导出 UIGF Json 文件到指定路径</value>
<value>UIGF Json ファイルを指定した場所へエクスポート</value>
</data>
<data name="ViewModelGachaLogUploadToHutaoCloudProgress" xml:space="preserve">
<value>胡桃クラウドにアップロード中</value>
</data>
<data name="ViewModelGachaUIGFImportPickerTitile" xml:space="preserve">
<value>导入 UIGF Json 文件</value>
<value>UIGF Json ファイルをインポート</value>
</data>
<data name="ViewModelGuideActionAgreement" xml:space="preserve">
<value>上記の規約を熟読し、それに同意します</value>
@@ -2012,6 +2012,9 @@
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>解像度</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioPlaceHolder" xml:space="preserve">
<value>快捷设置分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>枠の無いポップアップウィンドウとして作成します。</value>
</data>
@@ -2057,6 +2060,12 @@
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
<value>これらの設定はゲームが正常に起動した時のみ保存されます。</value>
</data>
<data name="ViewPageLaunchGameDiscordActivityDescription" xml:space="preserve">
<value>ゲームをプレイしている時に、Discord Activityのステータスを変更します。</value>
</data>
<data name="ViewPageLaunchGameDiscordActivityHeader" xml:space="preserve">
<value>Discord Activity</value>
</data>
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>ファイル</value>
</data>
@@ -2597,6 +2606,9 @@
<data name="ViewUserDocumentationHeader" xml:space="preserve">
<value>ドキュメント</value>
</data>
<data name="ViewUserNoUserHint" xml:space="preserve">
<value>尚未登录</value>
</data>
<data name="ViewUserRefreshCookieTokenSuccess" xml:space="preserve">
<value>CookieTokenを更新しました。</value>
</data>
@@ -2757,7 +2769,7 @@
<value>UIDは正しくありません</value>
</data>
<data name="WebEnkaResponseStatusCode404" xml:space="preserve">
<value>UID存在しません</value>
<value>ロール UID存在しません。時間を置いてもう一度試してください。</value>
</data>
<data name="WebEnkaResponseStatusCode424" xml:space="preserve">
<value>ゲームメンテナンス中</value>

View File

@@ -2012,6 +2012,9 @@
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioPlaceHolder" xml:space="preserve">
<value>快捷设置分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>테두리 없는 창모드</value>
</data>
@@ -2057,6 +2060,12 @@
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
<value>모든 설정은 게임을 성공적으로 실행한 후에 저장됩니다</value>
</data>
<data name="ViewPageLaunchGameDiscordActivityDescription" xml:space="preserve">
<value>在我游戏时设置 Discord Activity 状态</value>
</data>
<data name="ViewPageLaunchGameDiscordActivityHeader" xml:space="preserve">
<value>Discord Activity</value>
</data>
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>文件</value>
</data>
@@ -2597,6 +2606,9 @@
<data name="ViewUserDocumentationHeader" xml:space="preserve">
<value>文档</value>
</data>
<data name="ViewUserNoUserHint" xml:space="preserve">
<value>尚未登录</value>
</data>
<data name="ViewUserRefreshCookieTokenSuccess" xml:space="preserve">
<value>CookieToken을 동기화 했습니다</value>
</data>
@@ -2757,7 +2769,7 @@
<value>错误的 UID 格式</value>
</data>
<data name="WebEnkaResponseStatusCode404" xml:space="preserve">
<value>角色 UID 不存在</value>
<value>角色 UID 不存在,请稍候再试</value>
</data>
<data name="WebEnkaResponseStatusCode424" xml:space="preserve">
<value>游戏维护中</value>

View File

@@ -506,6 +506,9 @@
<data name="MustSelectUserAndUid" xml:space="preserve">
<value>必须先选择一个用户与角色</value>
</data>
<data name="ServerGachaLogServiceDeleteEntrySucceed" xml:space="preserve">
<value>删除了 Uid{0} 的 {1} 条祈愿记录</value>
</data>
<data name="ServerGachaLogServiceInsufficientRecordSlot" xml:space="preserve">
<value>胡桃云保存的祈愿记录存档数已达当前账号上限</value>
</data>
@@ -518,6 +521,12 @@
<data name="ServerGachaLogServiceServerDatabaseError" xml:space="preserve">
<value>数据异常,无法保存至云端,请勿跨账号上传或尝试删除云端数据后重试</value>
</data>
<data name="ServerGachaLogServiceUploadEntrySucceed" xml:space="preserve">
<value>上传了 Uid{0} 的 {1} 条祈愿记录,存储了 {2} 条</value>
</data>
<data name="ServerPassportLoginRequired" xml:space="preserve">
<value>请先登录或注册胡桃账号</value>
</data>
<data name="ServerPassportLoginSucceed" xml:space="preserve">
<value>登录成功</value>
</data>
@@ -2012,6 +2021,9 @@
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioPlaceHolder" xml:space="preserve">
<value>快捷设置分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>将窗口创建为弹出窗口,不带框架</value>
</data>
@@ -2057,6 +2069,12 @@
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
<value>所有选项仅会在启动游戏成功后保存</value>
</data>
<data name="ViewPageLaunchGameDiscordActivityDescription" xml:space="preserve">
<value>在我游戏时设置 Discord Activity 状态</value>
</data>
<data name="ViewPageLaunchGameDiscordActivityHeader" xml:space="preserve">
<value>Discord Activity</value>
</data>
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>文件</value>
</data>
@@ -2597,6 +2615,9 @@
<data name="ViewUserDocumentationHeader" xml:space="preserve">
<value>文档</value>
</data>
<data name="ViewUserNoUserHint" xml:space="preserve">
<value>尚未登录</value>
</data>
<data name="ViewUserRefreshCookieTokenSuccess" xml:space="preserve">
<value>刷新 CookieToken 成功</value>
</data>
@@ -2757,7 +2778,7 @@
<value>错误的 UID 格式</value>
</data>
<data name="WebEnkaResponseStatusCode404" xml:space="preserve">
<value>角色 UID 不存在</value>
<value>角色 UID 不存在,请稍候再试</value>
</data>
<data name="WebEnkaResponseStatusCode424" xml:space="preserve">
<value>游戏维护中</value>

View File

@@ -522,10 +522,10 @@
<value>登录成功</value>
</data>
<data name="ServerPassportRegisterSucceed" xml:space="preserve">
<value>注册成功</value>
<value>註冊成功</value>
</data>
<data name="ServerPassportResetPasswordSucceed" xml:space="preserve">
<value>新密码设置成功</value>
<value>新密碼設定成功</value>
</data>
<data name="ServerPassportServiceEmailHasNotRegistered" xml:space="preserve">
<value>当前邮箱尚未注册</value>
@@ -2012,6 +2012,9 @@
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioPlaceHolder" xml:space="preserve">
<value>快捷设置分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>將窗口創建為彈出窗口,不帶邊框</value>
</data>
@@ -2057,6 +2060,12 @@
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
<value>所有選項盡會在啓動游戲成功後保存</value>
</data>
<data name="ViewPageLaunchGameDiscordActivityDescription" xml:space="preserve">
<value>在我游戏时设置 Discord Activity 状态</value>
</data>
<data name="ViewPageLaunchGameDiscordActivityHeader" xml:space="preserve">
<value>Discord Activity</value>
</data>
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>文件</value>
</data>
@@ -2597,6 +2606,9 @@
<data name="ViewUserDocumentationHeader" xml:space="preserve">
<value>文檔</value>
</data>
<data name="ViewUserNoUserHint" xml:space="preserve">
<value>尚未登录</value>
</data>
<data name="ViewUserRefreshCookieTokenSuccess" xml:space="preserve">
<value>刷新 CookieToken 成功</value>
</data>
@@ -2757,7 +2769,7 @@
<value>錯誤的 UID 格式</value>
</data>
<data name="WebEnkaResponseStatusCode404" xml:space="preserve">
<value>角色 UID 不存在</value>
<value>角色UID不存在,請稍候再試</value>
</data>
<data name="WebEnkaResponseStatusCode424" xml:space="preserve">
<value>遊戲維護中</value>

View File

@@ -29,9 +29,9 @@ internal static class AppOptionsExtension
return true;
}
public static bool TryGetGameFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFileName)
public static bool TryGetGamePathAndGameFileName(this AppOptions appOptions, out string gamePath, [NotNullWhen(true)] out string? gameFileName)
{
string gamePath = appOptions.GamePath;
gamePath = appOptions.GamePath;
gameFileName = Path.GetFileName(gamePath);
if (string.IsNullOrEmpty(gameFileName))

View File

@@ -127,9 +127,6 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
{
WebDailyNote dailyNote = dailyNoteResponse.Data;
// 发送通知
await dailyNoteNotificationOperation.SendAsync(entry).ConfigureAwait(false);
// 集合内的实时便笺与数据库取出的非同一个对象,需要分别更新
// cache
await taskContext.SwitchToMainThreadAsync();
@@ -144,9 +141,14 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
}
// database
entry.UpdateDailyNote(dailyNote);
await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false);
await dailyNoteWebhookOperation.TryPostDailyNoteToWebhookAsync(dailyNote).ConfigureAwait(false);
{
entry.UpdateDailyNote(dailyNote);
// 发送通知必须早于数据库更新,否则会导致通知重复
await dailyNoteNotificationOperation.SendAsync(entry).ConfigureAwait(false);
await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false);
await dailyNoteWebhookOperation.TryPostDailyNoteToWebhookAsync(dailyNote).ConfigureAwait(false);
}
}
}
}

View File

@@ -10,22 +10,28 @@ namespace Snap.Hutao.Service.Discord;
internal static class DiscordController
{
// https://discord.com/developers/applications
private const long HutaoAppId = 1173950861647552623L;
private const long YuanshenId = 1175743396028088370L;
private const long GenshinImpactId = 1175747474384760962L;
private static readonly WaitCallback RunDiscordRunCallbacks = DiscordRunCallbacks;
private static readonly CancellationTokenSource StopTokenSource = new();
private static readonly object SyncRoot = new();
private static Snap.Discord.GameSDK.Discord? discordManager;
private static bool isInitialized;
public static async ValueTask<Result> ClearActivityAsync()
public static async ValueTask<Result> SetDefaultActivityAsync(DateTimeOffset startTime)
{
ResetManagerOrIgnore(HutaoAppId);
ActivityManager activityManager = discordManager.GetActivityManager();
return await activityManager.ClearActivityAsync().ConfigureAwait(false);
Activity activity = default;
activity.Timestamps.Start = startTime.ToUnixTimeSeconds();
activity.Assets.LargeImage = "icon";
activity.Assets.LargeText = SH.AppName;
return await activityManager.UpdateActivityAsync(activity).ConfigureAwait(false);
}
public static async ValueTask<Result> SetPlayingYuanShenAsync()
@@ -72,7 +78,13 @@ internal static class DiscordController
lock (SyncRoot)
{
StopTokenSource.Cancel();
discordManager?.Dispose();
try
{
discordManager?.Dispose();
}
catch (SEHException)
{
}
}
}
@@ -87,17 +99,16 @@ internal static class DiscordController
lock (SyncRoot)
{
discordManager?.Dispose();
discordManager = new(clientId, CreateFlags.NoRequireDiscord);
discordManager.SetLogHook(Snap.Discord.GameSDK.LogLevel.Debug, SetLogHookHandler.Create(&DebugWriteDiscordMessage));
}
discordManager = new(clientId, CreateFlags.NoRequireDiscord);
discordManager.SetLogHook(Snap.Discord.GameSDK.LogLevel.Debug, SetLogHookHandler.Create(&DebugWriteDiscordMessage));
if (isInitialized)
{
return;
}
ThreadPool.UnsafeQueueUserWorkItem(RunDiscordRunCallbacks, StopTokenSource.Token);
DiscordRunCallbacksAsync(StopTokenSource.Token).SafeForget();
isInitialized = true;
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
@@ -109,18 +120,46 @@ internal static class DiscordController
}
}
[SuppressMessage("", "SH007")]
private static void DiscordRunCallbacks(object? state)
private static async ValueTask DiscordRunCallbacksAsync(CancellationToken cancellationToken)
{
CancellationToken cancellationToken = (CancellationToken)state!;
while (!cancellationToken.IsCancellationRequested)
using (PeriodicTimer timer = new(TimeSpan.FromMilliseconds(1000)))
{
lock (SyncRoot)
try
{
discordManager?.RunCallbacks();
}
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
Thread.Sleep(100);
lock (SyncRoot)
{
try
{
discordManager?.RunCallbacks();
}
catch (ResultException ex)
{
// If result is Ok
// Maybe the connection is reset.
if (ex.Result is not Result.Ok)
{
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{ex.Result:D} {ex.Result}");
}
}
catch (SEHException ex)
{
// Known error codes:
// 0x80004005 E_FAIL
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:0x{ex.ErrorCode:X}");
}
}
}
}
catch (OperationCanceledException)
{
}
}
}
}

View File

@@ -1,12 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
namespace Snap.Hutao.Service.Discord;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IDiscordService))]
internal sealed partial class DiscordService : IDiscordService, IDisposable
{
private readonly RuntimeOptions runtimeOptions;
public async ValueTask SetPlayingActivity(bool isOversea)
{
_ = isOversea
? await DiscordController.SetPlayingGenshinImpactAsync().ConfigureAwait(false)
: await DiscordController.SetPlayingYuanShenAsync().ConfigureAwait(false);
}
public async ValueTask SetNormalActivity()
{
_ = await DiscordController.SetDefaultActivityAsync(runtimeOptions.AppLaunchTime).ConfigureAwait(false);
}
public void Dispose()
{
DiscordController.Stop();

View File

@@ -5,4 +5,7 @@ namespace Snap.Hutao.Service.Discord;
internal interface IDiscordService
{
ValueTask SetNormalActivity();
ValueTask SetPlayingActivity(bool isOversea);
}

View File

@@ -38,6 +38,7 @@ internal sealed class LaunchOptions : DbStoreOptions
private bool? isMonitorEnabled;
private AspectRatio? selectedAspectRatio;
private bool? useStarwardPlayTimeStatistics;
private bool? setDiscordActivityWhenPlaying;
/// <summary>
/// 构造一个新的启动游戏选项
@@ -190,6 +191,12 @@ internal sealed class LaunchOptions : DbStoreOptions
set => SetOption(ref useStarwardPlayTimeStatistics, SettingEntry.LaunchUseStarwardPlayTimeStatistics, value);
}
public bool SetDiscordActivityWhenPlaying
{
get => GetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, true);
set => SetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, value);
}
private static void InitializeMonitors(List<NameValue<int>> monitors)
{
// This list can't use foreach

View File

@@ -2,6 +2,8 @@
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Service.Discord;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Game.Unlocker;
using System.IO;
@@ -17,17 +19,18 @@ namespace Snap.Hutao.Service.Game.Process;
internal sealed partial class GameProcessService : IGameProcessService
{
private readonly IServiceProvider serviceProvider;
private readonly IDiscordService discordService;
private readonly RuntimeOptions runtimeOptions;
private readonly LaunchOptions launchOptions;
private readonly AppOptions appOptions;
private volatile int runningGamesCounter;
private volatile bool isGameRunning;
public bool IsGameRunning()
{
if (runningGamesCounter == 0)
if (isGameRunning)
{
return false;
return true;
}
return System.Diagnostics.Process.GetProcessesByName(YuanShenProcessName).Length > 0
@@ -41,21 +44,24 @@ internal sealed partial class GameProcessService : IGameProcessService
return;
}
string gamePath = appOptions.GamePath;
ArgumentException.ThrowIfNullOrEmpty(gamePath);
if (!appOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
{
ArgumentException.ThrowIfNullOrEmpty(gamePath);
return; // null check passing, actually never reach.
}
bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileName);
progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
using (System.Diagnostics.Process game = InitializeGameProcess(gamePath))
{
try
using (new GameRunningTracker(this, isOversea))
{
Interlocked.Increment(ref runningGamesCounter);
game.Start();
progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted));
if (launchOptions.UseStarwardPlayTimeStatistics && appOptions.TryGetGameFileName(out string? gameFileName))
if (launchOptions.UseStarwardPlayTimeStatistics)
{
bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileName);
await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false);
}
@@ -84,10 +90,6 @@ internal sealed partial class GameProcessService : IGameProcessService
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
}
}
finally
{
Interlocked.Decrement(ref runningGamesCounter);
}
}
}
@@ -131,4 +133,31 @@ internal sealed partial class GameProcessService : IGameProcessService
Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus)));
return unlocker.UnlockAsync(options, lockerProgress, token);
}
private class GameRunningTracker : IDisposable
{
private readonly GameProcessService service;
public GameRunningTracker(GameProcessService service, bool isOversea)
{
service.isGameRunning = true;
if (service.launchOptions.SetDiscordActivityWhenPlaying)
{
service.discordService.SetPlayingActivity(isOversea);
}
this.service = service;
}
public void Dispose()
{
if (service.launchOptions.SetDiscordActivityWhenPlaying)
{
service.discordService.SetNormalActivity();
}
service.isGameRunning = false;
}
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Process;
internal sealed class GameProcessTracker : IDisposable
{
private readonly Stack<IDisposable> disposables = [];
public TDisposable Track<TDisposable>(TDisposable disposable)
where TDisposable : IDisposable
{
disposables.Push(disposable);
return disposable;
}
public void Dispose()
{
while (disposables.TryPop(out IDisposable? disposable))
{
disposable.Dispose();
}
}
}

View File

@@ -21,9 +21,12 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
return;
}
if (!string.IsNullOrEmpty(user.Fingerprint))
if (user.Entity.FingerprintLastUpdateTime >= DateTimeOffset.UtcNow - TimeSpan.FromDays(3))
{
return;
if (!string.IsNullOrEmpty(user.Fingerprint))
{
return;
}
}
string model = Core.Random.GetUpperAndNumberString(6);
@@ -77,6 +80,8 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
Response<DeviceFpWrapper> response = await deviceFpClient.GetFingerprintAsync(data, token).ConfigureAwait(false);
user.Fingerprint = response.IsOk() ? response.Data.DeviceFp : string.Empty;
user.Entity.FingerprintLastUpdateTime = DateTimeOffset.UtcNow;
user.NeedDbUpdateAfterResume = true;
}
}

View File

@@ -87,7 +87,7 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
await userFingerprintService.TryInitializeAsync(user, token).ConfigureAwait(false);
// should not raise propery changed event here
// Should not raise propery changed event here
user.SetSelectedUserGameRole(user.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen), false);
return user.IsInitialized = true;
}
@@ -122,9 +122,12 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
private async ValueTask<bool> TrySetUserCookieTokenAsync(ViewModel.User.User user, CancellationToken token)
{
if (user.CookieToken is not null)
if (user.Entity.CookieTokenLastUpdateTime > DateTimeOffset.UtcNow - TimeSpan.FromDays(1))
{
return true;
if (user.CookieToken is not null)
{
return true;
}
}
Response<UidCookieToken> cookieTokenResponse = await serviceProvider
@@ -140,6 +143,9 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
[Cookie.ACCOUNT_ID] = user.Entity.Aid ?? string.Empty,
[Cookie.COOKIE_TOKEN] = cookieTokenResponse.Data.CookieToken,
};
user.Entity.CookieTokenLastUpdateTime = DateTimeOffset.UtcNow;
user.NeedDbUpdateAfterResume = true;
return true;
}
else

View File

@@ -38,6 +38,11 @@
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest" Condition="'$(ConfigurationName)'!='Debug'" />
<AppxManifest Include="Package.development.appxmanifest" Condition="'$(ConfigurationName)'=='Debug'" />
</ItemGroup>
<!-- Included Files -->
<ItemGroup>
@@ -78,6 +83,7 @@
<None Remove="Control\Panel\PanelSelector.xaml" />
<None Remove="Control\Theme\Card.xaml" />
<None Remove="Control\Theme\Color.xaml" />
<None Remove="Control\Theme\ComboBox.xaml" />
<None Remove="Control\Theme\Converter.xaml" />
<None Remove="Control\Theme\CornerRadius.xaml" />
<None Remove="Control\Theme\FlyoutStyle.xaml" />
@@ -129,6 +135,7 @@
<None Remove="View\Card\GachaStatisticsCard.xaml" />
<None Remove="View\Card\LaunchGameCard.xaml" />
<None Remove="View\Card\Primitive\CardProgressBar.xaml" />
<None Remove="View\Card\Primitive\HorizontalCard.xaml" />
<None Remove="View\Control\BaseValueSlider.xaml" />
<None Remove="View\Control\BottomTextControl.xaml" />
<None Remove="View\Control\DescParamComboBox.xaml" />
@@ -187,6 +194,11 @@
<AdditionalFiles Include="CodeMetricsConfig.txt" />
<AdditionalFiles Include="IdentityStructs.json" />
<AdditionalFiles Include="stylecop.json" />
<AdditionalFiles Include="Resource\Localization\SH.resx" />
<AdditionalFiles Include="Resource\Localization\SH.en.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ja.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ko.resx" />
<AdditionalFiles Include="Resource\Localization\SH.zh-Hant.resx" />
</ItemGroup>
<!-- Assets Files -->
@@ -278,11 +290,11 @@
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.0" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.1" />
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.2.0" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.4.0" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.507">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -306,15 +318,16 @@
</ItemGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
<None Include="Extension\EnumerableExtension.NameValueCollection.cs" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Resource\Localization\SH.resx" />
<AdditionalFiles Include="Resource\Localization\SH.en.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ja.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ko.resx" />
<AdditionalFiles Include="Resource\Localization\SH.zh-Hant.resx" />
<Page Update="View\Card\Primitive\HorizontalCard.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Control\Theme\ComboBox.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>

View File

@@ -5,6 +5,7 @@
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:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shvg="using:Snap.Hutao.ViewModel.Game"
@@ -60,14 +61,16 @@
Content="{StaticResource FontIconContentSetting}"
FontFamily="{StaticResource SymbolThemeFontFamily}"
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageHomeLaunchGameSettingAction}"/>
<ComboBox
<shc:SizeRestrictedContentControl
Grid.Row="2"
Grid.ColumnSpan="3"
VerticalAlignment="Bottom"
DisplayMemberPath="Name"
ItemsSource="{Binding GameAccounts}"
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
VerticalAlignment="Bottom">
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding GameAccounts}"
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
</Grid>
</Grid>
</Button>
</Button>

View File

@@ -0,0 +1,24 @@
<UserControl
x:Class="Snap.Hutao.View.Card.Primitive.HorizontalCard"
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"
mc:Ignorable="d">
<Border Style="{StaticResource BorderCardStyle}">
<Grid Background="{x:Bind Background, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Child="{x:Bind Left, Mode=OneWay}"/>
<Border
Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Child="{x:Bind Right, Mode=OneWay}"/>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.View.Card.Primitive;
[DependencyProperty("Left", typeof(UIElement), default!)]
[DependencyProperty("Right", typeof(UIElement), default!)]
internal sealed partial class HorizontalCard : UserControl
{
public HorizontalCard()
{
InitializeComponent();
}
}

View File

@@ -13,44 +13,44 @@
<x:Double x:Key="SettingsCardMinHeight">0</x:Double>
<DataTemplate x:Key="BaseValueTemplate">
<cwc:SettingsCard Margin="0,2,0,0" Header="{Binding Name}">
<cwc:SettingsCard
MinHeight="40"
Padding="16,0,42,0"
Header="{Binding Name}">
<TextBlock Text="{Binding Value}"/>
</cwc:SettingsCard>
</DataTemplate>
</UserControl.Resources>
<StackPanel>
<StackPanel VerticalAlignment="Top">
<cwc:SettingsCard Header="{shcm:ResourceString Name=ViewControlBaseValueSliderLevel}">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="8,0"
VerticalAlignment="Center"
Text="{x:Bind BaseValueInfo.CurrentLevelFormatted, Mode=OneWay}"/>
<CheckBox
MinWidth="0"
Margin="16,0,8,0"
Padding="8,0,0,0"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
Content="{shcm:ResourceString Name=ViewControlBaseValueSliderPromoted}"
IsChecked="{x:Bind BaseValueInfo.Promoted, Mode=TwoWay}"
Visibility="{x:Bind IsPromoteVisible, Converter={StaticResource BoolToVisibilityConverter}}"/>
<Slider
MinWidth="240"
Margin="16,0,8,0"
VerticalAlignment="Center"
Maximum="{x:Bind BaseValueInfo.MaxLevel, Mode=OneWay}"
Minimum="1"
StepFrequency="1"
Value="{x:Bind BaseValueInfo.CurrentLevel, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
<cwc:SettingsExpander
Header="{shcm:ResourceString Name=ViewControlBaseValueSliderLevel}"
IsExpanded="True"
ItemTemplate="{StaticResource BaseValueTemplate}"
ItemsSource="{x:Bind BaseValueInfo.Values, Mode=OneWay}">
<StackPanel
Height="16"
Margin="8,0,0,0"
Orientation="Horizontal"
Spacing="16">
<TextBlock VerticalAlignment="Center" Text="{x:Bind BaseValueInfo.CurrentLevelFormatted, Mode=OneWay}"/>
<CheckBox
MinWidth="0"
Margin="0,-8,0,-8"
Padding="8,0,0,0"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
Content="{shcm:ResourceString Name=ViewControlBaseValueSliderPromoted}"
IsChecked="{x:Bind BaseValueInfo.Promoted, Mode=TwoWay}"
Visibility="{x:Bind IsPromoteVisible, Converter={StaticResource BoolToVisibilityConverter}}"/>
<Slider
MinWidth="240"
Margin="0,-16,0,-16"
VerticalAlignment="Center"
Maximum="{x:Bind BaseValueInfo.MaxLevel, Mode=OneWay}"
Minimum="1"
StepFrequency="1"
Value="{x:Bind BaseValueInfo.CurrentLevel, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsExpander>
<ItemsControl
VerticalAlignment="Top"
ItemTemplate="{StaticResource BaseValueTemplate}"
ItemsSource="{x:Bind BaseValueInfo.Values, Mode=OneWay}"/>
</StackPanel>
</UserControl>

View File

@@ -5,35 +5,38 @@
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
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:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shmm="using:Snap.Hutao.Model.Metadata"
mc:Ignorable="d">
<UserControl.Resources>
<Thickness x:Key="SettingsCardPadding">16,8</Thickness>
<x:Double x:Key="SettingsCardMinHeight">0</x:Double>
<x:Double x:Key="SettingsCardWrapThreshold">0</x:Double>
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
<DataTemplate x:Key="ParameterDescriptionTemplate" x:DataType="shmm:ParameterDescription">
<cwc:SettingsCard Margin="0,3,0,0" Header="{Binding Description}">
<cwc:SettingsCard
MinHeight="40"
Padding="16,0,42,0"
Header="{Binding Description}">
<TextBlock Text="{Binding Parameter}"/>
</cwc:SettingsCard>
</DataTemplate>
</UserControl.Resources>
<StackPanel>
<StackPanel Margin="0,0,0,0" VerticalAlignment="Top">
<cwc:SettingsCard Header="{shcm:ResourceString Name=ViewControlBaseValueSliderLevel}">
<ComboBox
DisplayMemberPath="Level"
ItemsSource="{x:Bind Source, Mode=OneWay}"
SelectedItem="{x:Bind SelectedItem, Mode=TwoWay}"
Style="{StaticResource SettingsContentComboBoxStyle}"/>
</cwc:SettingsCard>
</StackPanel>
<ItemsControl
VerticalAlignment="Top"
ItemContainerTransitions="{StaticResource ContentThemeTransitions}"
ItemTemplate="{StaticResource ParameterDescriptionTemplate}"
ItemsSource="{x:Bind SelectedItem.Parameters, Mode=OneWay}"/>
</StackPanel>
<cwc:SettingsExpander
Header="{shcm:ResourceString Name=ViewControlBaseValueSliderLevel}"
IsExpanded="True"
ItemTemplate="{StaticResource ParameterDescriptionTemplate}"
ItemsSource="{x:Bind SelectedItem.Parameters, Mode=OneWay}"
Visibility="{x:Bind SelectedItem.Parameters.Count, Converter={StaticResource Int32ToVisibilityConverter}, Mode=OneWay}">
<shc:SizeRestrictedContentControl Margin="0,-8">
<ComboBox
x:Name="LevelSelectorComboBox"
DisplayMemberPath="Level"
SelectionChanged="OnLevelSelectorComboBoxSelectionChanged"
Style="{StaticResource SettingsContentComboBoxStyle}"/>
</shc:SizeRestrictedContentControl>
</cwc:SettingsExpander>
</UserControl>

View File

@@ -30,8 +30,16 @@ internal sealed partial class DescParamComboBox : UserControl
{
if (args.NewValue != args.OldValue && args.NewValue is List<LevelParameters<string, ParameterDescription>> list)
{
descParamComboBox.SelectedItem = list.ElementAtOrLastOrDefault(descParamComboBox.PreferredSelectedIndex);
LevelParameters<string, ParameterDescription>? target = list.ElementAtOrLastOrDefault(descParamComboBox.PreferredSelectedIndex);
descParamComboBox.SelectedItem = target;
descParamComboBox.LevelSelectorComboBox.ItemsSource = list;
descParamComboBox.LevelSelectorComboBox.SelectedItem = target;
}
}
}
}
private void OnLevelSelectorComboBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItem = (LevelParameters<string, ParameterDescription>)((ComboBox)sender).SelectedItem;
}
}

View File

@@ -2,6 +2,7 @@
x:Class="Snap.Hutao.View.Control.SkillPivot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shch="using:Snap.Hutao.Control.Helper"
@@ -17,22 +18,18 @@
</Style>
<DataTemplate x:Key="SkillHeaderTemplate">
<StackPanel>
<StackPanel Background="Transparent" ToolTipService.ToolTip="{Binding Name}">
<shci:MonoChrome shch:FrameworkElementHelper.SquareLength="36" Source="{Binding Icon, Converter={StaticResource SkillIconConverter}}"/>
<TextBlock
Margin="0,8,0,0"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Name}"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Pivot
HeaderTemplate="{StaticResource SkillHeaderTemplate}"
ItemTemplate="{x:Bind ItemTemplate}"
ItemsSource="{x:Bind Skills, Mode=OneWay}"
SelectedItem="{x:Bind Selected}"
Style="{StaticResource DefaultPivotStyle}"/>
<StackPanel>
<cwc:Segmented
x:Name="SkillSelectorSegmented"
HorizontalAlignment="Stretch"
SelectionChanged="OnSkillSelectorSegmentedSelectionChanged"
ItemTemplate="{StaticResource SkillHeaderTemplate}"/>
<ContentPresenter Content="{x:Bind Selected, Mode=OneWay}" ContentTemplate="{x:Bind ItemTemplate}"/>
</StackPanel>
</UserControl>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections;
@@ -11,7 +12,7 @@ namespace Snap.Hutao.View.Control;
/// 技能展柜
/// </summary>
[HighQuality]
[DependencyProperty("Skills", typeof(IList))]
[DependencyProperty("Skills", typeof(IList), null, nameof(OnSkillsChanged))]
[DependencyProperty("Selected", typeof(object))]
[DependencyProperty("ItemTemplate", typeof(DataTemplate))]
internal sealed partial class SkillPivot : UserControl
@@ -23,4 +24,22 @@ internal sealed partial class SkillPivot : UserControl
{
InitializeComponent();
}
private static void OnSkillsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
if (sender is SkillPivot skillPivot)
{
if (args.OldValue != args.NewValue && args.NewValue as IList is [object target, ..] list)
{
skillPivot.Selected = target;
skillPivot.SkillSelectorSegmented.ItemsSource = list;
skillPivot.SkillSelectorSegmented.SelectedItem = target;
}
}
}
private void OnSkillSelectorSegmentedSelectionChanged(object sender, SelectionChangedEventArgs e)
{
Selected = ((Segmented)sender).SelectedItem;
}
}

View File

@@ -247,13 +247,13 @@
</CommandBar.Content>
<AppBarElementContainer>
<ComboBox
Height="36"
MinWidth="120"
Margin="2,6,3,6"
DisplayMemberPath="Name"
ItemsSource="{Binding Archives, Mode=OneWay}"
SelectedItem="{Binding SelectedArchive, Mode=TwoWay}"/>
<shc:SizeRestrictedContentControl Margin="2,6,3,6">
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding Archives, Mode=OneWay}"
SelectedItem="{Binding SelectedArchive, Mode=TwoWay}"
Style="{ThemeResource CommandBarComboBoxStyle}"/>
</shc:SizeRestrictedContentControl>
</AppBarElementContainer>
<AppBarButton
Command="{Binding AddArchiveCommand}"

View File

@@ -248,13 +248,14 @@
<Pivot.RightHeader>
<CommandBar DefaultLabelPosition="Right">
<AppBarElementContainer>
<ComboBox
Height="36"
MinWidth="160"
Margin="6,6,6,6"
DisplayMemberPath="Name"
ItemsSource="{Binding Projects}"
SelectedItem="{Binding SelectedProject, Mode=TwoWay}"/>
<shc:SizeRestrictedContentControl Margin="6,6,6,6">
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding Projects}"
SelectedItem="{Binding SelectedProject, Mode=TwoWay}"
Style="{ThemeResource CommandBarComboBoxStyle}"/>
</shc:SizeRestrictedContentControl>
</AppBarElementContainer>
<AppBarButton
Command="{Binding AddProjectCommand}"

View File

@@ -231,13 +231,13 @@
IsHitTestVisible="False"/>
<Pivot>
<Pivot.LeftHeader>
<ComboBox
Height="36"
MinWidth="120"
Margin="16,6,0,6"
DisplayMemberPath="Uid"
ItemsSource="{Binding Archives}"
SelectedItem="{Binding SelectedArchive, Mode=TwoWay}"/>
<shc:SizeRestrictedContentControl Margin="16,6,0,6">
<ComboBox
DisplayMemberPath="Uid"
ItemsSource="{Binding Archives}"
SelectedItem="{Binding SelectedArchive, Mode=TwoWay}"
Style="{ThemeResource CommandBarComboBoxStyle}"/>
</shc:SizeRestrictedContentControl>
</Pivot.LeftHeader>
<Pivot.RightHeader>
<CommandBar DefaultLabelPosition="Right">

View File

@@ -171,13 +171,14 @@
</cwc:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding HutaoOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<shccs:ComboBox2
MinWidth="320"
DisplayMemberPath="DisplayName"
EnableMemberPath="IsNotCompatOnly"
ItemsSource="{Binding KnownSchemes}"
SelectedItem="{Binding SelectedScheme, Mode=TwoWay}"
Style="{StaticResource DefaultComboBoxStyle}"/>
<shc:SizeRestrictedContentControl>
<shccs:ComboBox2
DisplayMemberPath="DisplayName"
EnableMemberPath="IsNotCompatOnly"
ItemsSource="{Binding KnownSchemes}"
SelectedItem="{Binding SelectedScheme, Mode=TwoWay}"
Style="{StaticResource DefaultComboBoxStyle}"/>
</shc:SizeRestrictedContentControl>
</StackPanel>
</cwc:SettingsCard>
@@ -217,11 +218,12 @@
<ToggleSwitch Width="120" IsOn="{Binding Options.IsBorderless, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioHeader}">
<ComboBox
Width="156"
Margin="0,0,136,0"
ItemsSource="{Binding Options.AspectRatios}"
SelectedItem="{Binding Options.SelectedAspectRatio, Mode=TwoWay}"/>
<shc:SizeRestrictedContentControl Margin="0,0,136,0">
<ComboBox
ItemsSource="{Binding Options.AspectRatios}"
PlaceholderText="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioPlaceHolder}"
SelectedItem="{Binding Options.SelectedAspectRatio, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenWidthDescription}" Header="-screen-width">
<StackPanel Orientation="Horizontal" Spacing="16">
@@ -247,13 +249,13 @@
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameMonitorsDescription}" Header="-monitor">
<StackPanel Orientation="Horizontal" Spacing="16">
<ComboBox
Width="156"
VerticalAlignment="Center"
DisplayMemberPath="Name"
IsEnabled="{Binding Options.IsMonitorEnabled}"
ItemsSource="{Binding Options.Monitors}"
SelectedItem="{Binding Options.Monitor, Mode=TwoWay}"/>
<shc:SizeRestrictedContentControl>
<ComboBox
DisplayMemberPath="Name"
IsEnabled="{Binding Options.IsMonitorEnabled}"
ItemsSource="{Binding Options.Monitors}"
SelectedItem="{Binding Options.Monitor, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
<ToggleSwitch Width="120" IsOn="{Binding Options.IsMonitorEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
@@ -290,6 +292,12 @@
HeaderIcon="{shcm:FontIcon Glyph=&#xEC92;}">
<ToggleSwitch Width="120" IsOn="{Binding Options.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE8CF;}">
<ToggleSwitch Width="120" IsOn="{Binding Options.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
</cwc:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>

View File

@@ -192,19 +192,23 @@
Description="{shcm:ResourceString Name=ViewPageSettingApperanceLanguageDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingApperanceLanguageHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xF2B7;}">
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding Options.Cultures}"
SelectedItem="{Binding SelectedCulture, Mode=TwoWay}"/>
<shc:SizeRestrictedContentControl>
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding Options.Cultures}"
SelectedItem="{Binding SelectedCulture, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
</cwc:SettingsCard>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageSettingBackdropMaterialDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingBackdropMaterialHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE7F7;}">
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding Options.BackdropTypes}"
SelectedItem="{Binding SelectedBackdropType, Mode=TwoWay}"/>
<shc:SizeRestrictedContentControl>
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding Options.BackdropTypes}"
SelectedItem="{Binding SelectedBackdropType, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
</cwc:SettingsCard>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingKeyShortcutHeader}"/>
@@ -240,13 +244,14 @@
Content="Alt"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasAlt, Mode=TwoWay}"/>
</cwc:UniformGrid>
<ComboBox
MinWidth="120"
VerticalAlignment="Center"
DisplayMemberPath="Name"
ItemsSource="{Binding HotKeyOptions.VirtualKeys}"
SelectedItem="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.KeyNameValue, Mode=TwoWay}"
Style="{StaticResource DefaultComboBoxStyle}"/>
<shc:SizeRestrictedContentControl>
<ComboBox
MinWidth="120"
VerticalAlignment="Center"
DisplayMemberPath="Name"
ItemsSource="{Binding HotKeyOptions.VirtualKeys}"
SelectedItem="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.KeyNameValue, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
<ToggleSwitch
MinWidth="120"
VerticalAlignment="Center"

View File

@@ -116,7 +116,7 @@
PlaceholderText="Please input link"
Text="{Binding Announcement.Link, Mode=TwoWay}"/>
<TextBox
Header="Link"
Header="Version Threshold"
PlaceholderText="Max present version(leave empty to present in any version)"
Text="{Binding Announcement.MaxPresentVersion, Mode=TwoWay}"/>
<TextBox
@@ -124,14 +124,15 @@
Header="Content"
PlaceholderText="Please input content"
Text="{Binding Announcement.Content, Mode=TwoWay}"/>
<ComboBox
Header="Severity"
ItemsSource="{cwh:EnumValues Type=InfoBarSeverity}"
SelectedItem="{Binding Announcement.Severity, Mode=TwoWay}"/>
<shc:SizeRestrictedContentControl>
<ComboBox
Header="Severity"
ItemsSource="{cwh:EnumValues Type=InfoBarSeverity}"
SelectedItem="{Binding Announcement.Severity, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
<Button Command="{Binding UploadAnnouncementCommand}" Content="Upload"/>
</StackPanel>
</Expander>
</StackPanel>
</ScrollViewer>
</shc:ScopedPage>

View File

@@ -15,6 +15,7 @@
xmlns:shcp="using:Snap.Hutao.Control.Panel"
xmlns:shct="using:Snap.Hutao.Control.Text"
xmlns:shvc="using:Snap.Hutao.View.Control"
xmlns:shvcp="using:Snap.Hutao.View.Card.Primitive"
xmlns:shvw="using:Snap.Hutao.ViewModel.Wiki"
d:DataContext="{d:DesignInstance Type=shvw:WikiAvatarViewModel}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
@@ -24,21 +25,24 @@
</mxi:Interaction.Behaviors>
<Page.Resources>
<DataTemplate x:Key="SkillDataTemplate">
<Grid Margin="0,12,0,0">
<StackPanel Grid.Column="0">
<shct:DescriptionTextBlock Margin="0,0,0,16" Description="{Binding Description}">
<shct:DescriptionTextBlock.Resources>
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</shct:DescriptionTextBlock.Resources>
</shct:DescriptionTextBlock>
<shvc:DescParamComboBox
Grid.Column="0"
HorizontalAlignment="Stretch"
PreferredSelectedIndex="9"
Source="{Binding Proud, Mode=OneWay, Converter={StaticResource DescParamDescriptor}}"/>
</StackPanel>
<Grid Margin="0,16,0,0" ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<shct:DescriptionTextBlock VerticalAlignment="Top" Description="{Binding Description}">
<shct:DescriptionTextBlock.Resources>
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</shct:DescriptionTextBlock.Resources>
</shct:DescriptionTextBlock>
<shvc:DescParamComboBox
Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
PreferredSelectedIndex="9"
Source="{Binding Proud, Mode=OneWay, Converter={StaticResource DescParamDescriptor}}"/>
</Grid>
</DataTemplate>
@@ -80,66 +84,110 @@
</DataTemplate>
<DataTemplate x:Key="CultivationItemTemplate">
<shvc:BottomTextControl Text="{Binding Name}">
<shvc:ItemIcon Icon="{Binding Icon, Converter={StaticResource ItemIconConverter}}" Quality="{Binding RankLevel}"/>
</shvc:BottomTextControl>
<shvcp:HorizontalCard>
<shvcp:HorizontalCard.Left>
<shvc:ItemIcon
Width="40"
Height="40"
Icon="{Binding Icon, Converter={StaticResource ItemIconConverter}}"
Quality="{Binding RankLevel}"/>
</shvcp:HorizontalCard.Left>
<shvcp:HorizontalCard.Right>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Name}"/>
</shvcp:HorizontalCard.Right>
</shvcp:HorizontalCard>
</DataTemplate>
<DataTemplate x:Key="CollocationTemplate">
<shvc:BottomTextControl Text="{Binding Rate}" ToolTipService.ToolTip="{Binding Name}">
<shvc:ItemIcon Icon="{Binding Icon}" Quality="{Binding Quality}"/>
</shvc:BottomTextControl>
<shvcp:HorizontalCard>
<shvcp:HorizontalCard.Left>
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
</shvcp:HorizontalCard.Left>
<shvcp:HorizontalCard.Right>
<StackPanel Margin="8,0,0,0" HorizontalAlignment="Left">
<TextBlock Text="{Binding Name}"/>
<TextBlock
Opacity="0.7"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Rate}"/>
</StackPanel>
</shvcp:HorizontalCard.Right>
</shvcp:HorizontalCard>
</DataTemplate>
<DataTemplate x:Key="CollocationReliquaryTemplate">
<shvc:BottomTextControl Text="{Binding Rate}" ToolTipService.ToolTip="{Binding Name}">
<cwc:SwitchPresenter Value="{Binding Icons.Count, Mode=OneWay}">
<cwc:Case IsDefault="True">
<cwc:Case.Value>
<x:Int32>0</x:Int32>
</cwc:Case.Value>
<Grid>
<shvc:ItemIcon
Width="80"
Height="80"
Icon="{StaticResource UI_ItemIcon_None}"
Quality="QUALITY_ORANGE"/>
</Grid>
</cwc:Case>
<cwc:Case>
<cwc:Case.Value>
<x:Int32>1</x:Int32>
</cwc:Case.Value>
<Grid>
<shvc:ItemIcon
Width="80"
Height="80"
Icon="{Binding Icons[0]}"
Quality="QUALITY_ORANGE"/>
</Grid>
</cwc:Case>
<cwc:Case>
<cwc:Case.Value>
<x:Int32>2</x:Int32>
</cwc:Case.Value>
<Grid>
<shvc:ItemIcon Quality="QUALITY_ORANGE"/>
<shci:CachedImage
Width="54"
Margin="0,4,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Source="{Binding Icons[0]}"/>
<shci:CachedImage
Width="54"
Margin="0,0,0,4"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Source="{Binding Icons[1]}"/>
</Grid>
</cwc:Case>
</cwc:SwitchPresenter>
</shvc:BottomTextControl>
<shvcp:HorizontalCard>
<shvcp:HorizontalCard.Left>
<cwc:SwitchPresenter Value="{Binding Icons.Count, Mode=OneWay}">
<cwc:Case IsDefault="True">
<cwc:Case.Value>
<x:Int32>0</x:Int32>
</cwc:Case.Value>
<Grid>
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{StaticResource UI_ItemIcon_None}"
Quality="QUALITY_ORANGE"/>
</Grid>
</cwc:Case>
<cwc:Case>
<cwc:Case.Value>
<x:Int32>1</x:Int32>
</cwc:Case.Value>
<Grid>
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{Binding Icons[0]}"
Quality="QUALITY_ORANGE"/>
</Grid>
</cwc:Case>
<cwc:Case>
<cwc:Case.Value>
<x:Int32>2</x:Int32>
</cwc:Case.Value>
<Grid>
<shvc:ItemIcon
Width="48"
Height="48"
Quality="QUALITY_ORANGE"/>
<shci:CachedImage
Width="32"
Margin="2,2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Source="{Binding Icons[0]}"/>
<shci:CachedImage
Width="32"
Margin="0,0,2,2"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Source="{Binding Icons[1]}"/>
</Grid>
</cwc:Case>
</cwc:SwitchPresenter>
</shvcp:HorizontalCard.Left>
<shvcp:HorizontalCard.Right>
<StackPanel Margin="8,0,8,0" HorizontalAlignment="Left">
<TextBlock
Text="{Binding Name}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"/>
<TextBlock
Opacity="0.7"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Rate}"/>
</StackPanel>
</shvcp:HorizontalCard.Right>
</shvcp:HorizontalCard>
</DataTemplate>
<DataTemplate x:Key="CostumeTemplate">
@@ -169,14 +217,11 @@
</DataTemplate>
<DataTemplate x:Key="FetterStoryTemplate">
<StackPanel Margin="0,0,0,0">
<TextBlock Text="{Binding Title}"/>
<shct:DescriptionTextBlock
Margin="0,8,0,0"
Description="{Binding Context}"
TextStyle="{StaticResource CaptionTextBlockStyle}"/>
<MenuFlyoutSeparator Margin="0,8"/>
</StackPanel>
<cwc:SettingsCard Padding="16,8,8,8" Header="{Binding Title}">
<cwc:SettingsCard.Description>
<shct:DescriptionTextBlock Description="{Binding Context}" TextStyle="{StaticResource CaptionTextBlockStyle}"/>
</cwc:SettingsCard.Description>
</cwc:SettingsCard>
</DataTemplate>
<DataTemplate x:Key="AvatarGridTemplate">
@@ -194,7 +239,7 @@
</Grid.RowDefinitions>
<CommandBar
Grid.Row="0"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{x:Null}"
BorderThickness="0"
DefaultLabelPosition="Right">
@@ -239,7 +284,7 @@
DisplayMode="Inline"
IsPaneOpen="True"
OpenPaneLength="{StaticResource CompatSplitViewOpenPaneLength}"
PaneBackground="{StaticResource CardBackgroundFillColorSecondaryBrush}">
PaneBackground="{ThemeResource CardBackgroundFillColorSecondaryBrush}">
<SplitView.Pane>
<ListView
Grid.Row="1"
@@ -264,315 +309,392 @@
<ScrollViewer>
<StackPanel
MaxWidth="800"
Margin="0,0,16,16"
HorizontalAlignment="Left">
Padding="32"
HorizontalAlignment="Left"
Spacing="16">
<!-- 简介 -->
<Grid Margin="16,16,0,16" VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<Grid Margin="0,0,0,16">
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Grid Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}" Style="{ThemeResource GridCardStyle}">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="16">
<Grid Margin="0,0,0,12" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<shci:MonoChrome
Grid.Column="0"
Width="32"
Height="32"
HorizontalAlignment="Left"
Source="{Binding Selected.FetterInfo.VisionBefore, Converter={StaticResource ElementNameIconConverter}}"/>
<shci:MonoChrome
Grid.Column="1"
Width="32"
Height="32"
Source="{Binding Selected.Weapon, Converter={StaticResource WeaponTypeIconConverter}}"/>
</Grid>
<shvc:ItemIcon
Width="128"
Height="128"
Icon="{Binding Selected.Icon, Converter={StaticResource AvatarIconConverter}, Mode=OneWay}"
Quality="{Binding Selected.Quality, Mode=OneWay}"/>
</StackPanel>
<StackPanel Grid.Column="1" Margin="16">
<StackPanel Margin="0,0,0,2" Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding Selected.Name}"/>
<TextBlock
Margin="24,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding Selected.FetterInfo.Title}"/>
</StackPanel>
<TextBlock
Opacity="0.7"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.Detail}"
TextWrapping="NoWrap"/>
<cwc:UniformGrid
Margin="0,16,0,0"
ColumnSpacing="6"
Columns="4">
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarOccupationNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.Native}"
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarConstellationNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.ConstellationBefore}"
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarDateofBirthTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.BirthFormatted}"
TextWrapping="NoWrap"/>
</StackPanel>
</cwc:UniformGrid>
<cwc:UniformGrid
Margin="0,12,0,0"
ColumnSpacing="6"
Columns="4">
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarChineseCVNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.CvChinese}"
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarJapaneseCVNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.CvJapanese}"
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarEnglishCVNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.CvEnglish}"
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarKoreanCVNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.CvKorean}"
TextWrapping="NoWrap"/>
</StackPanel>
</cwc:UniformGrid>
</StackPanel>
<StackPanel Grid.Row="1" Grid.ColumnSpan="2">
<ItemsControl
Margin="16,0,16,16"
ItemTemplate="{StaticResource CultivationItemTemplate}"
ItemsSource="{Binding Selected.CultivationItemsView}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwc:UniformGrid
ColumnSpacing="8"
Columns="3"
RowSpacing="8"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
</Grid>
</Border>
<!-- 属性 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}" CornerRadius="{ThemeResource ControlCornerRadius}">
<Border.Resources>
<SolidColorBrush x:Key="ToggleButtonBackground" Color="Transparent"/>
<SolidColorBrush x:Key="ExpanderContentBackground" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackground" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackgroundDisabled" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackgroundPointerOver" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackgroundPressed" Color="Transparent"/>
</Border.Resources>
<shvc:BaseValueSlider
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
BaseValueInfo="{Binding BaseValueInfo, Mode=OneWay}"/>
</Border>
</Border>
<!-- 天赋 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border
Padding="16"
Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}"
CornerRadius="{ThemeResource ControlCornerRadius}">
<shvc:SkillPivot ItemTemplate="{StaticResource SkillDataTemplate}" Skills="{Binding Selected.SkillDepot.CompositeSkills}"/>
</Border>
</Border>
<!-- 命座 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border
Padding="16"
Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}"
CornerRadius="{ThemeResource ControlCornerRadius}">
<shvc:SkillPivot ItemTemplate="{StaticResource TalentDataTemplate}" Skills="{Binding Selected.SkillDepot.Talents}"/>
</Border>
</Border>
<!-- 搭配 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border
Padding="16"
Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}"
CornerRadius="{ThemeResource ControlCornerRadius}">
<Grid ColumnSpacing="8" RowSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shci:MonoChrome
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Width="32"
Height="32"
Source="{Binding Selected.FetterInfo.VisionBefore, Converter={StaticResource ElementNameIconConverter}}"/>
<shci:MonoChrome
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarTeamCombinationHeader}"/>
<ItemsControl
Grid.Row="1"
Grid.Column="0"
ItemTemplate="{StaticResource CollocationTemplate}"
ItemsPanel="{StaticResource StackPanelSpacing4Template}"
ItemsSource="{Binding Selected.Collocation.Avatars}"/>
<TextBlock
Grid.Row="0"
Grid.Column="1"
Width="32"
Height="32"
Source="{Binding Selected.Weapon, Converter={StaticResource WeaponTypeIconConverter}}"/>
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarWeaponCombinationHeader}"/>
<ItemsControl
Grid.Row="1"
Grid.Column="1"
ItemTemplate="{StaticResource CollocationTemplate}"
ItemsPanel="{StaticResource StackPanelSpacing4Template}"
ItemsSource="{Binding Selected.Collocation.Weapons}"/>
<TextBlock
Grid.Row="0"
Grid.Column="2"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarArtifactSetCombinationHeader}"/>
<ItemsControl
Grid.Row="1"
Grid.Column="2"
ItemTemplate="{StaticResource CollocationReliquaryTemplate}"
ItemsPanel="{StaticResource StackPanelSpacing4Template}"
ItemsSource="{Binding Selected.Collocation.ReliquarySets}"/>
</Grid>
<shvc:ItemIcon
Width="100"
Height="100"
Icon="{Binding Selected.Icon, Converter={StaticResource AvatarIconConverter}, Mode=OneWay}"
Quality="{Binding Selected.Quality, Mode=OneWay}"/>
</StackPanel>
</Border>
</Border>
<StackPanel Grid.Column="1" Margin="16,0,0,0">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding Selected.Name}"/>
<TextBlock
Margin="24,0,0,2"
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding Selected.FetterInfo.Title}"/>
</StackPanel>
<TextBlock
Margin="0,12,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.Detail}"
TextWrapping="NoWrap"/>
<cwc:UniformGrid
Margin="0,12,0,0"
ColumnSpacing="12"
Columns="4">
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarOccupationNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.Native}"
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarConstellationNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.ConstellationBefore}"
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarDateofBirthTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.BirthFormatted}"
TextWrapping="NoWrap"/>
</StackPanel>
</cwc:UniformGrid>
<cwc:UniformGrid
Margin="0,12,0,0"
ColumnSpacing="12"
Columns="4">
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarChineseCVNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.CvChinese}"
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarJapaneseCVNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.CvJapanese}"
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarEnglishCVNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.CvEnglish}"
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageWiKiAvatarKoreanCVNameTitle}"/>
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Selected.FetterInfo.CvKorean}"
TextWrapping="NoWrap"/>
</StackPanel>
</cwc:UniformGrid>
</StackPanel>
</Grid>
<!-- 属性 -->
<shvc:BaseValueSlider
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
BaseValueInfo="{Binding BaseValueInfo, Mode=OneWay}"/>
<TextBlock
Margin="16,32,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarAscensionMaterialsHeader}"/>
<GridView
Margin="16,16,0,0"
ItemTemplate="{StaticResource CultivationItemTemplate}"
ItemsSource="{Binding Selected.CultivationItemsView}"
SelectionMode="None"/>
<TextBlock
Margin="16,32,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarTanlentHeader}"/>
<shvc:SkillPivot
Margin="16,16,0,0"
ItemTemplate="{StaticResource SkillDataTemplate}"
Skills="{Binding Selected.SkillDepot.CompositeSkills}"/>
<TextBlock
Margin="16,32,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarConstellationNameTitle}"/>
<shvc:SkillPivot
Grid.Column="1"
Margin="16,16,0,0"
ItemTemplate="{StaticResource TalentDataTemplate}"
Skills="{Binding Selected.SkillDepot.Talents}"/>
<TextBlock
Margin="16,32,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarTeamCombinationHeader}"/>
<GridView
Margin="16,16,0,0"
ItemTemplate="{StaticResource CollocationTemplate}"
ItemsSource="{Binding Selected.Collocation.Avatars}"
SelectionMode="None"/>
<TextBlock
Margin="16,0,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarWeaponCombinationHeader}"/>
<GridView
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
ItemTemplate="{StaticResource CollocationTemplate}"
ItemsSource="{Binding Selected.Collocation.Weapons}"
SelectionMode="None"/>
<TextBlock
Margin="16,0,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarArtifactSetCombinationHeader}"/>
<GridView
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
ItemTemplate="{StaticResource CollocationReliquaryTemplate}"
ItemsSource="{Binding Selected.Collocation.ReliquarySets}"
SelectionMode="None"/>
<TextBlock
Margin="16,32,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarMiscHeader}"/>
<Border Margin="16,16,0,0" cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<!-- 立绘 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<cwc:ConstrainedBox AspectRatio="2048:1024">
<Grid Style="{ThemeResource GridCardStyle}">
<Grid Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}" Style="{ThemeResource GridCardStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!-- 使其有较低的ZOrder -->
<shci:CachedImage Grid.ColumnSpan="2" Source="{Binding Selected.Icon, Converter={StaticResource GachaAvatarImgConverter}}"/>
<shci:CachedImage
Grid.ColumnSpan="2"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Source="{Binding Selected.Icon, Converter={StaticResource GachaAvatarImgConverter}}"/>
<Border Margin="16" Style="{StaticResource BorderCardStyle}">
<shci:CachedImage
HorizontalAlignment="Stretch"
CornerRadius="{ThemeResource ControlCornerRadius}"
Source="{Binding Selected.Icon, Converter={StaticResource GachaAvatarIconConverter}}"/>
<Border
Grid.Column="0"
Margin="16"
Style="{StaticResource BorderCardStyle}">
<cwc:ConstrainedBox AspectRatio="320:1024">
<shci:CachedImage CornerRadius="{ThemeResource ControlCornerRadius}" Source="{Binding Selected.Icon, Converter={StaticResource GachaAvatarIconConverter}}"/>
</cwc:ConstrainedBox>
</Border>
</Grid>
</cwc:ConstrainedBox>
</Border>
<!-- 料理 -->
<Expander
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Header="{shcm:ResourceString Name=ViewPageWiKiAvatarFoodHeader}">
<Grid DataContext="{Binding Selected.CookBonusView}">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarSpecialFoodTitle}"/>
<shvc:BottomTextControl
Grid.Row="1"
Grid.Column="0"
Margin="0,16,0,0"
Text="{Binding Item.Name}">
<shvc:ItemIcon Icon="{Binding Item.Icon, Converter={StaticResource ItemIconConverter}}" Quality="{Binding Item.RankLevel}"/>
</shvc:BottomTextControl>
<TextBlock
Grid.Column="1"
Margin="16,0,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarOriginalFoodTitle}"/>
<shvc:BottomTextControl
Grid.Row="1"
Grid.Column="1"
Margin="16,16,0,0"
Text="{Binding OriginItem.Name}">
<shvc:ItemIcon Icon="{Binding OriginItem.Icon, Converter={StaticResource ItemIconConverter}}" Quality="{Binding OriginItem.RankLevel}"/>
</shvc:BottomTextControl>
<StackPanel
Grid.RowSpan="4"
Grid.Column="2"
Margin="16,0,0,0">
<TextBlock
Grid.Row="2"
Grid.ColumnSpan="4"
Text="{Binding Item.Description}"
TextWrapping="Wrap"/>
<TextBlock
Grid.Row="3"
Grid.ColumnSpan="4"
Margin="0,16,0,0"
Text="{Binding Item.EffectDescription}"
TextWrapping="Wrap"/>
</StackPanel>
</Grid>
</Expander>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}" CornerRadius="{ThemeResource ControlCornerRadius}">
<Border.Resources>
<SolidColorBrush x:Key="ExpanderContentBackground" Color="Transparent"/>
<SolidColorBrush x:Key="ExpanderHeaderBackground" Color="Transparent"/>
</Border.Resources>
<Expander
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Header="{shcm:ResourceString Name=ViewPageWiKiAvatarFoodHeader}"
IsExpanded="True">
<Grid DataContext="{Binding Selected.CookBonusView}">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarSpecialFoodTitle}"/>
<shvc:BottomTextControl
Grid.Row="1"
Grid.Column="0"
Margin="0,16,0,0"
Text="{Binding Item.Name}">
<shvc:ItemIcon Icon="{Binding Item.Icon, Converter={StaticResource ItemIconConverter}}" Quality="{Binding Item.RankLevel}"/>
</shvc:BottomTextControl>
<TextBlock
Grid.Column="1"
Margin="16,0,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarOriginalFoodTitle}"/>
<shvc:BottomTextControl
Grid.Row="1"
Grid.Column="1"
Margin="16,16,0,0"
Text="{Binding OriginItem.Name}">
<shvc:ItemIcon Icon="{Binding OriginItem.Icon, Converter={StaticResource ItemIconConverter}}" Quality="{Binding OriginItem.RankLevel}"/>
</shvc:BottomTextControl>
<StackPanel
Grid.RowSpan="4"
Grid.Column="2"
Margin="16,0,0,0">
<TextBlock
Grid.Row="2"
Grid.ColumnSpan="4"
Text="{Binding Item.Description}"
TextWrapping="Wrap"/>
<TextBlock
Grid.Row="3"
Grid.ColumnSpan="4"
Margin="0,16,0,0"
Text="{Binding Item.EffectDescription}"
TextWrapping="Wrap"/>
</StackPanel>
</Grid>
</Expander>
</Border>
</Border>
<!-- 衣装 -->
<Expander
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Header="{shcm:ResourceString Name=ViewPageWiKiAvatarCostumeHeader}">
<ItemsControl
Margin="0,0,0,-16"
ItemTemplate="{StaticResource CostumeTemplate}"
ItemsSource="{Binding Selected.Costumes}"/>
</Expander>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}" CornerRadius="{ThemeResource ControlCornerRadius}">
<Border.Resources>
<SolidColorBrush x:Key="ExpanderContentBackground" Color="Transparent"/>
<SolidColorBrush x:Key="ExpanderHeaderBackground" Color="Transparent"/>
</Border.Resources>
<Expander
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Header="{shcm:ResourceString Name=ViewPageWiKiAvatarCostumeHeader}"
IsExpanded="True">
<ItemsControl
Margin="0,0,0,-16"
ItemTemplate="{StaticResource CostumeTemplate}"
ItemsSource="{Binding Selected.Costumes}"/>
</Expander>
</Border>
</Border>
<!-- 资料 -->
<Expander
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Header="{shcm:ResourceString Name=ViewPageWiKiAvatarQuotesHeader}">
<ItemsControl
Margin="0,0,0,-24"
ItemTemplate="{StaticResource FetterStoryTemplate}"
ItemsSource="{Binding Selected.FetterInfo.Fetters}"/>
</Expander>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}" CornerRadius="{ThemeResource ControlCornerRadius}">
<Border.Resources>
<SolidColorBrush x:Key="ToggleButtonBackground" Color="Transparent"/>
<SolidColorBrush x:Key="ExpanderContentBackground" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackground" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackgroundDisabled" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackgroundPointerOver" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackgroundPressed" Color="Transparent"/>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Hidden">
<cwc:SettingsExpander
Header="{shcm:ResourceString Name=ViewPageWiKiAvatarQuotesHeader}"
ItemTemplate="{StaticResource FetterStoryTemplate}"
ItemsSource="{Binding Selected.FetterInfo.Fetters}"/>
</ScrollViewer>
</Border>
</Border>
<!-- 故事 -->
<Expander
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Header="{shcm:ResourceString Name=ViewPageWiKiAvatarStoriesHeader}">
<ItemsControl
Margin="0,0,0,-24"
ItemTemplate="{StaticResource FetterStoryTemplate}"
ItemsSource="{Binding Selected.FetterInfo.FetterStories}"/>
</Expander>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}" CornerRadius="{ThemeResource ControlCornerRadius}">
<Border.Resources>
<SolidColorBrush x:Key="ToggleButtonBackground" Color="Transparent"/>
<SolidColorBrush x:Key="ExpanderContentBackground" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackground" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackgroundDisabled" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackgroundPointerOver" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackgroundPressed" Color="Transparent"/>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Hidden">
<cwc:SettingsExpander
Header="{shcm:ResourceString Name=ViewPageWiKiAvatarStoriesHeader}"
ItemTemplate="{StaticResource FetterStoryTemplate}"
ItemsSource="{Binding Selected.FetterInfo.FetterStories}"/>
</ScrollViewer>
</Border>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>

View File

@@ -32,18 +32,23 @@
VerticalAlignment="Center"
IsChecked="{x:Bind HotKeyOptions.IsMouseClickRepeatForeverOn, Mode=OneWay}"
IsHitTestVisible="False"
Visibility="{x:Bind HotKeyOptions.MouseClickRepeatForeverKeyCombination.IsEnabled, Converter={StaticResource BoolToVisibilityConverter}}">
Visibility="{x:Bind HotKeyOptions.MouseClickRepeatForeverKeyCombination.IsEnabled, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{x:Bind HotKeyOptions.MouseClickRepeatForeverKeyCombination.DisplayName, Mode=OneWay}"/>
<TextBlock Grid.Column="1" Text="{shcm:ResourceString Name=ViewTitleAutoClicking}"/>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{shcm:ResourceString Name=ViewTitleAutoClicking}"/>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind HotKeyOptions.MouseClickRepeatForeverKeyCombination.DisplayName, Mode=OneWay}"/>
</Grid>
</ToggleButton>
</StackPanel>
</Grid>
</UserControl>
</UserControl>

View File

@@ -107,6 +107,14 @@
Text="{Binding SelectedUser.UserInfo.Nickname, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"/>
<TextBlock
Grid.Column="1"
Margin="1,0,0,0"
VerticalAlignment="Center"
Text="{shcm:ResourceString Name=ViewUserNoUserHint}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{Binding Users.Count, Converter={StaticResource Int32ToVisibilityRevertConverter}}"/>
<FontIcon
Grid.Column="2"
Margin="0,0,8,0"

View File

@@ -66,7 +66,7 @@ internal sealed partial class AchievementImporter
{
ValueResult<bool, ValueFile> pickerResult = fileSystemPickerInteraction.PickFile(
SH.ServiceAchievementUIAFImportPickerTitile,
[(SH.ServiceAchievementUIAFImportPickerFilterText, ".json")]);
[(SH.ServiceAchievementUIAFImportPickerFilterText, "*.json")]);
if (pickerResult.TryGetValue(out ValueFile file))
{

View File

@@ -27,20 +27,14 @@ internal sealed class ReliquarySetView
{
StringBuilder nameBuilder = new();
List<Uri> icons = new(2);
foreach (ReliquarySet set in CollectionsMarshal.AsSpan(sets))
foreach (ref readonly ReliquarySet set in CollectionsMarshal.AsSpan(sets))
{
Model.Metadata.Reliquary.ReliquarySet metaSet = idReliquarySetMap[set.EquipAffixId];
if (nameBuilder.Length > 0)
{
nameBuilder.AppendLine();
}
nameBuilder.Append(set.Count).Append('×').Append(metaSet.Name);
nameBuilder.Append(set.Count).Append('×').Append(metaSet.Name).Append('+');
icons.Add(RelicIconConverter.IconNameToUri(metaSet.Icon));
}
Name = nameBuilder.ToString();
Name = nameBuilder.ToString(0, nameBuilder.Length - 1);
Icons = icons;
}
else

View File

@@ -128,7 +128,7 @@ internal sealed partial class HutaoCloudViewModel : Abstraction.ViewModel
[Command("NavigateToSpiralAbyssRecordCommand")]
private void NavigateToSpiralAbyssRecord()
{
navigationService.Navigate<View.Page.SpiralAbyssRecordPage>(INavigationAwaiter.Default);
navigationService.Navigate<View.Page.SpiralAbyssRecordPage>(INavigationAwaiter.Default, true);
}
private async ValueTask RefreshUidCollectionAsync()

View File

@@ -46,30 +46,30 @@ internal static class StaticResource
private static readonly ApplicationDataCompositeValue LatestResourceVersionMap = new()
{
{ "AchievementIcon", 1 },
{ "AvatarCard", 0 },
{ "AvatarIcon", 2 },
{ "Bg", 1 },
{ "ChapterIcon", 0 },
{ "AvatarCard", 1 },
{ "AvatarIcon", 3 },
{ "Bg", 2 },
{ "ChapterIcon", 1 },
{ "CodexMonster", 0 },
{ "Costume", 0 },
{ "EmotionIcon", 0 },
{ "EquipIcon", 1 },
{ "GachaAvatarIcon", 1 },
{ "GachaAvatarImg", 1 },
{ "GachaEquipIcon", 1 },
{ "Costume", 1 },
{ "EmotionIcon", 1 },
{ "EquipIcon", 2 },
{ "GachaAvatarIcon", 2 },
{ "GachaAvatarImg", 2 },
{ "GachaEquipIcon", 2 },
{ "GcgCharAvatarIcon", 0 },
{ "IconElement", 1 },
{ "ItemIcon", 1 },
{ "LoadingPic", 0 },
{ "MonsterIcon", 1 },
{ "MonsterSmallIcon", 0 },
{ "NameCardIcon", 0 },
{ "NameCardPic", 1 },
{ "IconElement", 2 },
{ "ItemIcon", 2 },
{ "LoadingPic", 1 },
{ "MonsterIcon", 2 },
{ "MonsterSmallIcon", 1 },
{ "NameCardIcon", 1 },
{ "NameCardPic", 2 },
{ "NameCardPicAlpha", 0 },
{ "Property", 1 },
{ "RelicIcon", 1 },
{ "Skill", 1 },
{ "Talent", 1 },
{ "RelicIcon", 2 },
{ "Skill", 2 },
{ "Talent", 2 },
};
public static void FulfillAll()

View File

@@ -7,4 +7,6 @@ internal sealed class LatestPackage : Package
{
[JsonPropertyName("segments")]
public List<PackageSegment> Segments { get; set; } = default!;
public new string DisplayName { get => Name; }
}

View File

@@ -8,6 +8,7 @@ using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Web.Response;
using System.IO;
using System.Net.Http;
using System.Text;
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
@@ -46,8 +47,15 @@ internal sealed partial class ResourceClient
// 补全缺失的信息
if (resp is { Data.Game.Latest: LatestPackage latest })
{
latest.Path = latest.Segments[0].Path[..^4]; // .00X
latest.Name = Path.GetFileName(latest.Path);
StringBuilder pathBuilder = new();
foreach (PackageSegment segment in latest.Segments)
{
pathBuilder.AppendLine(segment.Path);
}
latest.Path = pathBuilder.ToStringTrimEndReturn();
string path = latest.Segments[0].Path[..^4]; // .00X
latest.Name = Path.GetFileName(path);
}
return Response.Response.DefaultIfNull(resp);

View File

@@ -34,7 +34,7 @@ internal sealed partial class CardClient
public async ValueTask<Response<VerificationRegistration>> CreateVerificationAsync(User user, CancellationToken token)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.CardCreateVerification(false))
.SetRequestUri(ApiEndpoints.CardCreateVerification(true))
.SetUserCookieAndFpHeader(user, CookieType.LToken)
.Get();

View File

@@ -40,6 +40,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordDailyNote(userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.Get();
await builder.SignDataAsync(DataSignAlgorithmVersion.Gen2, SaltType.X4, false).ConfigureAwait(false);
@@ -60,6 +61,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordDailyNote(userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.SetXrpcChallenge(challenge)
.Get();
@@ -86,6 +88,8 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordIndex(userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.Get();
await builder.SignDataAsync(DataSignAlgorithmVersion.Gen2, SaltType.X4, false).ConfigureAwait(false);
@@ -106,6 +110,8 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordIndex(userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.SetXrpcChallenge(challenge)
.Get();
@@ -133,6 +139,8 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.Get();
await builder.SignDataAsync(DataSignAlgorithmVersion.Gen2, SaltType.X4, false).ConfigureAwait(false);
@@ -153,6 +161,8 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.SetXrpcChallenge(challenge)
.Get();
@@ -179,6 +189,8 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordRoleBasicInfo(userAndUid.Uid))
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.Get();
await builder.SignDataAsync(DataSignAlgorithmVersion.Gen2, SaltType.X4, false).ConfigureAwait(false);
@@ -203,6 +215,8 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordCharacter)
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.PostJson(new CharacterData(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id)));
await builder.SignDataAsync(DataSignAlgorithmVersion.Gen2, SaltType.X4, false).ConfigureAwait(false);
@@ -223,6 +237,8 @@ internal sealed partial class GameRecordClient : IGameRecordClient
HttpRequestMessageBuilder verifiedBuilder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.GameRecordCharacter)
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.SetXrpcChallenge(challenge)
.PostJson(new CharacterData(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id)));