diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.csproj b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.csproj index 5b81c9af..e4b92cd8 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.csproj +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -13,7 +13,7 @@ - + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs new file mode 100644 index 00000000..a7090302 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/CollectionsMarshalTest.cs @@ -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 dictionaryValueKeyRefValue = []; + Dictionary dictionaryValueKeyValueValue = []; + Dictionary dictionaryRefKeyValueValue = []; + Dictionary dictionaryRefKeyRefValue = []; +#else + Dictionary dictionaryValueKeyRefValue = new(); + Dictionary dictionaryValueKeyValueValue = new(); + Dictionary dictionaryRefKeyValueValue = new(); + Dictionary 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 dictionaryValueKeyRefValue = []; + Dictionary dictionaryValueKeyValueValue = []; + Dictionary dictionaryRefKeyValueValue = []; + Dictionary dictionaryRefKeyRefValue = []; +#else + Dictionary dictionaryValueKeyRefValue = new(); + Dictionary dictionaryValueKeyValueValue = new(); + Dictionary dictionaryRefKeyValueValue = new(); + Dictionary 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); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs index f680883f..40dd8c3a 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/JsonSerializeTest.cs @@ -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() { diff --git a/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs new file mode 100644 index 00000000..aaa2b7ce --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Test/IncomingFeature/GeniusInvokationDecoding.cs @@ -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; } + + /// + /// https://www.bilibili.com/video/av278125720 + /// + [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 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]; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj b/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj index 88f4159f..6a15892b 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj +++ b/src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Snap.Hutao/Snap.Hutao/.filenesting.json b/src/Snap.Hutao/Snap.Hutao/.filenesting.json index f88118b1..8093d3cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/.filenesting.json +++ b/src/Snap.Hutao/Snap.Hutao/.filenesting.json @@ -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" + ] } } } diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml b/src/Snap.Hutao/Snap.Hutao/App.xaml index b15eeb24..a7c38922 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml @@ -10,6 +10,7 @@ + diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs index 9cbc5d20..c049e8aa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CompositionImage.cs @@ -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 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() - { - } - /// /// 更新视觉对象 /// @@ -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; - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs index ac491620..f7fac999 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/MonoChrome.cs @@ -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) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs index e9e17beb..96f69e44 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs @@ -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 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) { diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs index bd191648..31f8a0ab 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayoutState.cs @@ -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]; } /// @@ -74,6 +72,21 @@ internal sealed class UniformStaggeredLayoutState /// 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(); } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs b/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs new file mode 100644 index 00000000..986de656 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/SizeRestrictedContentControl.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/ComboBox.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/ComboBox.xaml new file mode 100644 index 00000000..babd8d20 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/ComboBox.xaml @@ -0,0 +1,11 @@ + + + + 0 + diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/FontStyle.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/FontStyle.xaml index df613a0d..bffb9f4e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/FontStyle.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/FontStyle.xaml @@ -93,4 +93,19 @@ + + + 1,0 + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/ItemsPanelTemplate.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/ItemsPanelTemplate.xaml index 28a32452..2778290c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/ItemsPanelTemplate.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/ItemsPanelTemplate.xaml @@ -17,6 +17,9 @@ + + + +{ + ref readonly TData GetPinnableReference(); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs index d9b10b3a..4f975aa0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs @@ -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"); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs index 186f6917..45906a33 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs @@ -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(); + await taskContext.SwitchToBackgroundAsync(); + serviceProvider .GetRequiredService() .As()? @@ -176,6 +179,11 @@ internal sealed partial class Activation : IActivation .As()? .InitializeInternalAsync() .SafeForget(); + + serviceProvider + .GetRequiredService() + .SetNormalActivity() + .SafeForget(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs b/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs index f5a89430..49c50cab 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptions.cs @@ -34,6 +34,8 @@ internal sealed class RuntimeOptions : IOptions { 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 /// public bool IsElevated { get => isElevated ??= GetElevated(); } + public DateTimeOffset AppLaunchTime { get; } + /// public RuntimeOptions Value { get => this; } diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs similarity index 72% rename from src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtensions.cs rename to src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs index b6d987e7..515666c7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/StringBuilderExtension.cs @@ -10,7 +10,7 @@ namespace Snap.Hutao.Extension; /// 扩展方法 /// [HighQuality] -internal static class StringBuilderExtensions +internal static class StringBuilderExtension { /// /// 当条件符合时执行 @@ -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); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs b/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs index d88586a0..0b005287 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/WinRTExtension.cs @@ -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); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs index 85e8f047..2e720421 100644 --- a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs @@ -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 logger; diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20231126113631_AddUserLastUpdateTime.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20231126113631_AddUserLastUpdateTime.Designer.cs new file mode 100644 index 00000000..de6d08e3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20231126113631_AddUserLastUpdateTime.Designer.cs @@ -0,0 +1,555 @@ +// +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 + { + /// + 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("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("achievement_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CalculatorRefreshTime") + .HasColumnType("TEXT"); + + b.Property("GameRecordRefreshTime") + .HasColumnType("TEXT"); + + b.Property("Info") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ShowcaseRefreshTime") + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("avatar_infos"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("cultivate_entries"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("IsFinished") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("EntryId"); + + b.ToTable("cultivate_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AttachedUid") + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("cultivate_projects"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DailyNote") + .HasColumnType("TEXT"); + + b.Property("DailyTaskNotify") + .HasColumnType("INTEGER"); + + b.Property("DailyTaskNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionNotify") + .HasColumnType("INTEGER"); + + b.Property("ExpeditionNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("HomeCoinNotifyThreshold") + .HasColumnType("INTEGER"); + + b.Property("RefreshTime") + .HasColumnType("TEXT"); + + b.Property("ResinNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("ResinNotifyThreshold") + .HasColumnType("INTEGER"); + + b.Property("TransformerNotify") + .HasColumnType("INTEGER"); + + b.Property("TransformerNotifySuppressed") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("UserId"); + + b.ToTable("daily_notes"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("gacha_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("GachaType") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("QueryType") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("gacha_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AttachUid") + .HasColumnType("TEXT"); + + b.Property("MihoyoSDK") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.ToTable("game_accounts"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AppendPropIdList") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("MainPropId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_reliquaries"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("PromoteLevel") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.HasIndex("ProjectId"); + + b.ToTable("inventory_weapons"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ExpireTime") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("object_cache"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("settings"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SpiralAbyss") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Uid") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("spiral_abysses"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Aid") + .HasColumnType("TEXT"); + + b.Property("CookieToken") + .HasColumnType("TEXT"); + + b.Property("CookieTokenLastUpdateTime") + .HasColumnType("TEXT"); + + b.Property("Fingerprint") + .HasColumnType("TEXT"); + + b.Property("FingerprintLastUpdateTime") + .HasColumnType("TEXT"); + + b.Property("IsOversea") + .HasColumnType("INTEGER"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LToken") + .HasColumnType("TEXT") + .HasColumnName("Ltoken"); + + b.Property("Mid") + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20231126113631_AddUserLastUpdateTime.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20231126113631_AddUserLastUpdateTime.cs new file mode 100644 index 00000000..745d8d5e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20231126113631_AddUserLastUpdateTime.cs @@ -0,0 +1,42 @@ +// +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + /// + public partial class AddUserLastUpdateTime : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + 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( + 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))); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CookieTokenLastUpdateTime", + table: "users"); + + migrationBuilder.DropColumn( + name: "FingerprintLastUpdateTime", + table: "users"); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs index dd8d37f4..6c6a556f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs @@ -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("CookieToken") .HasColumnType("TEXT"); + b.Property("CookieTokenLastUpdateTime") + .HasColumnType("TEXT"); + b.Property("Fingerprint") .HasColumnType("TEXT"); + b.Property("FingerprintLastUpdateTime") + .HasColumnType("TEXT"); + b.Property("IsOversea") .HasColumnType("INTEGER"); diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs index b6e00850..97474eb7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs @@ -106,6 +106,8 @@ internal sealed partial class SettingEntry public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics"; + public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying"; + /// /// 启动游戏 多倍启动 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs index 0b9f7bf0..1ae7fdd8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs @@ -65,6 +65,10 @@ internal sealed class User : ISelectable, IMappingFrom /// public string? Fingerprint { get; set; } + public DateTimeOffset FingerprintLastUpdateTime { get; set; } + + public DateTimeOffset CookieTokenLastUpdateTime { get; set; } + /// /// 创建一个新的用户 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Frozen/IntrinsicFrozen.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Frozen/IntrinsicFrozen.cs index 21a0946d..6963ff37 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Frozen/IntrinsicFrozen.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Frozen/IntrinsicFrozen.cs @@ -14,32 +14,32 @@ internal static class IntrinsicFrozen /// /// 所属地区 /// - public static readonly FrozenSet AssociationTypes = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToFrozenSet(); + public static FrozenSet AssociationTypes { get; } = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToFrozenSet(); /// /// 武器类型 /// - public static readonly FrozenSet WeaponTypes = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToFrozenSet(); + public static FrozenSet WeaponTypes { get; } = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToFrozenSet(); /// /// 物品类型 /// - public static readonly FrozenSet ItemQualities = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToFrozenSet(); + public static FrozenSet ItemQualities { get; } = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToFrozenSet(); /// /// 身材类型 /// - public static readonly FrozenSet BodyTypes = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToFrozenSet(); + public static FrozenSet BodyTypes { get; } = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToFrozenSet(); /// /// 战斗属性 /// - public static readonly FrozenSet FightProperties = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToFrozenSet(); + public static FrozenSet FightProperties { get; } = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToFrozenSet(); /// /// 元素名称 /// - public static readonly FrozenSet ElementNames = FrozenSet.ToFrozenSet( + public static FrozenSet ElementNames { get; } = FrozenSet.ToFrozenSet( [ SH.ModelIntrinsicElementNameFire, SH.ModelIntrinsicElementNameWater, @@ -50,7 +50,7 @@ internal static class IntrinsicFrozen SH.ModelIntrinsicElementNameRock, ]); - public static readonly FrozenSet MaterialTypeDescriptions = FrozenSet.ToFrozenSet( + public static FrozenSet MaterialTypeDescriptions { get; } = FrozenSet.ToFrozenSet( [ SH.ModelMetadataMaterialCharacterAndWeaponEnhancementMaterial, SH.ModelMetadataMaterialCharacterEXPMaterial, diff --git a/src/Snap.Hutao/Snap.Hutao/Package.StoreAssociation.xml b/src/Snap.Hutao/Snap.Hutao/Package.StoreAssociation.xml index 1386cb9a..7f484ba9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.StoreAssociation.xml +++ b/src/Snap.Hutao/Snap.Hutao/Package.StoreAssociation.xml @@ -14,8 +14,5 @@ 胡桃工具箱 - - 60568DGPStudio.SnapGenshinResin - \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Package.development.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.development.appxmanifest new file mode 100644 index 00000000..4cb960fd --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Package.development.appxmanifest @@ -0,0 +1,68 @@ + + + + + + + + Snap Hutao Dev + DGP Studio + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 胡桃 Dev + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx index 67723dd2..b623366c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx @@ -948,7 +948,7 @@ Unable to find cached metadata file - HTTP {0} | Error:{1}:元数据校验文件下载失败 + HTTP {0} | Error {1}: Failed to download metadata verification file Metadata service has not been initialized or failed to initialize @@ -2012,6 +2012,9 @@ Screen Resolution + + Resolution Quick Set + Create window as popup, without frame @@ -2057,6 +2060,12 @@ All options will be saved only after the game is launched successfully. + + Set My Discord Activity Status When I'm in the Game + + + Discord Activity + File @@ -2597,6 +2606,9 @@ Document + + No Login + Refresh CookieToken successfully @@ -2757,7 +2769,7 @@ Wrong UID format - Role UID does not exist + Role UID does not exist, please try again later Game in maintenance diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ja.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ja.resx index a0bcfab8..52243f29 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ja.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ja.resx @@ -130,7 +130,7 @@ キャンセル - 完了 + OK 確定 @@ -600,10 +600,10 @@ {0} つのアチーブメントを追加 | {1} つのアチーブメントを更新 |{2} つのアチーブメントを削除 - UIAF Json 文件 + UIAF Json ファイル - 打开 UIAF Json 文件 + UIAF Json ファイルを開く 複数の同一アチーブメント Idがアーカイブに混在しています @@ -792,10 +792,10 @@ 参量物質変化器は使用可能 - 正在提瓦特大陆中探索 + テイワット大陸を探索中 - 由 {0} 启动 + スタートから {0} {0}:祈願履歴を確認できません @@ -882,7 +882,7 @@ ゲーム本体を選択する - 游戏本体 + ゲームクライアント Unity ログファイルが見つかりません @@ -948,7 +948,7 @@ キャッシュされたメタデータファイルが見つかりませんでした - HTTP {0} | Error:{1}:元数据校验文件下载失败 + HTTP {0} | Error {1}: メタデータ検証ファイルのダウンロードに失敗しました。 メタデータサービスが初期化されていないか、または初期化できませんでした @@ -1350,7 +1350,7 @@ {0} を削除してもよろしいでしょうか? - 导出 UIAF Json 文件到指定路径 + UIAF Json ファイルを指定した場所へエクスポート 素材リスト取得中、しばらくお待ちください @@ -1479,13 +1479,13 @@ 胡桃クラウドで祈願履歴を同期します - 导出 UIGF Json 文件到指定路径 + UIGF Json ファイルを指定した場所へエクスポート 胡桃クラウドにアップロード中 - 导入 UIGF Json 文件 + UIGF Json ファイルをインポート 上記の規約を熟読し、それに同意します @@ -2012,6 +2012,9 @@ 解像度 + + 快捷设置分辨率 + 枠の無いポップアップウィンドウとして作成します。 @@ -2057,6 +2060,12 @@ これらの設定はゲームが正常に起動した時のみ保存されます。 + + ゲームをプレイしている時に、Discord Activityのステータスを変更します。 + + + Discord Activity + ファイル @@ -2597,6 +2606,9 @@ ドキュメント + + 尚未登录 + CookieTokenを更新しました。 @@ -2757,7 +2769,7 @@ UIDは正しくありません - UID は存在しません + ロール UIDが存在しません。時間を置いてもう一度試してください。 ゲームメンテナンス中 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ko.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ko.resx index 658cc101..fdb28141 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ko.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.ko.resx @@ -2012,6 +2012,9 @@ 分辨率 + + 快捷设置分辨率 + 테두리 없는 창모드 @@ -2057,6 +2060,12 @@ 모든 설정은 게임을 성공적으로 실행한 후에 저장됩니다 + + 在我游戏时设置 Discord Activity 状态 + + + Discord Activity + 文件 @@ -2597,6 +2606,9 @@ 文档 + + 尚未登录 + CookieToken을 동기화 했습니다 @@ -2757,7 +2769,7 @@ 错误的 UID 格式 - 角色 UID 不存在 + 角色 UID 不存在,请稍候再试 游戏维护中 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 5ff9d3c8..596cea0a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -506,6 +506,9 @@ 必须先选择一个用户与角色 + + 删除了 Uid:{0} 的 {1} 条祈愿记录 + 胡桃云保存的祈愿记录存档数已达当前账号上限 @@ -518,6 +521,12 @@ 数据异常,无法保存至云端,请勿跨账号上传或尝试删除云端数据后重试 + + 上传了 Uid:{0} 的 {1} 条祈愿记录,存储了 {2} 条 + + + 请先登录或注册胡桃账号 + 登录成功 @@ -2012,6 +2021,9 @@ 分辨率 + + 快捷设置分辨率 + 将窗口创建为弹出窗口,不带框架 @@ -2057,6 +2069,12 @@ 所有选项仅会在启动游戏成功后保存 + + 在我游戏时设置 Discord Activity 状态 + + + Discord Activity + 文件 @@ -2597,6 +2615,9 @@ 文档 + + 尚未登录 + 刷新 CookieToken 成功 @@ -2757,7 +2778,7 @@ 错误的 UID 格式 - 角色 UID 不存在 + 角色 UID 不存在,请稍候再试 游戏维护中 diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx index 3cea4d20..70377b8a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.zh-Hant.resx @@ -522,10 +522,10 @@ 登录成功 - 注册成功 + 註冊成功 - 新密码设置成功 + 新密碼設定成功 当前邮箱尚未注册 @@ -2012,6 +2012,9 @@ 分辨率 + + 快捷设置分辨率 + 將窗口創建為彈出窗口,不帶邊框 @@ -2057,6 +2060,12 @@ 所有選項盡會在啓動游戲成功後保存 + + 在我游戏时设置 Discord Activity 状态 + + + Discord Activity + 文件 @@ -2597,6 +2606,9 @@ 文檔 + + 尚未登录 + 刷新 CookieToken 成功 @@ -2757,7 +2769,7 @@ 錯誤的 UID 格式 - 角色 UID 不存在 + 角色UID不存在,請稍候再試 遊戲維護中 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs index 70e34e60..0359ed1d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs @@ -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)) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs index 6cbf2fb0..1112ef38 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs @@ -127,9 +127,6 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient ClearActivityAsync() + public static async ValueTask 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 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) + { + } } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs index 16de3814..8f1654d3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs @@ -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(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs index e80f0a1f..46717554 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs @@ -5,4 +5,7 @@ namespace Snap.Hutao.Service.Discord; internal interface IDiscordService { + ValueTask SetNormalActivity(); + + ValueTask SetPlayingActivity(bool isOversea); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index aa265ef3..5876b870 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -38,6 +38,7 @@ internal sealed class LaunchOptions : DbStoreOptions private bool? isMonitorEnabled; private AspectRatio? selectedAspectRatio; private bool? useStarwardPlayTimeStatistics; + private bool? setDiscordActivityWhenPlaying; /// /// 构造一个新的启动游戏选项 @@ -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> monitors) { // This list can't use foreach diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs index a30c6c77..ed27178f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs @@ -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 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; + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessTracker.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessTracker.cs new file mode 100644 index 00000000..2c698e61 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessTracker.cs @@ -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 disposables = []; + + public TDisposable Track(TDisposable disposable) + where TDisposable : IDisposable + { + disposables.Push(disposable); + return disposable; + } + + public void Dispose() + { + while (disposables.TryPop(out IDisposable? disposable)) + { + disposable.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs index 4a2be536..864346e5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs @@ -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 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; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserInitializationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserInitializationService.cs index 42398f38..08fc3785 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserInitializationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserInitializationService.cs @@ -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 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 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 diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index d4d8061b..8400f6ce 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -38,6 +38,11 @@ x64 Debug;Release + + + + + @@ -78,6 +83,7 @@ + @@ -129,6 +135,7 @@ + @@ -187,6 +194,11 @@ + + + + + @@ -278,11 +290,11 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -306,15 +318,16 @@ - - - - - - - + + MSBuild:Compile + + + + + MSBuild:Compile + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml index 10e7d0ef..bc97e50c 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml @@ -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}"/> - + VerticalAlignment="Bottom"> + + - + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Card/Primitive/HorizontalCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Card/Primitive/HorizontalCard.xaml new file mode 100644 index 00000000..14c6d654 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Card/Primitive/HorizontalCard.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Card/Primitive/HorizontalCard.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Card/Primitive/HorizontalCard.xaml.cs new file mode 100644 index 00000000..b31a5606 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Card/Primitive/HorizontalCard.xaml.cs @@ -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(); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/BaseValueSlider.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/BaseValueSlider.xaml index d50800c2..aeb7ba4b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/BaseValueSlider.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/BaseValueSlider.xaml @@ -13,44 +13,44 @@ 0 - + - - - - - - - - - + + + + + + - - diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/DescParamComboBox.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/DescParamComboBox.xaml index b3636856..a48d31d2 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/DescParamComboBox.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/DescParamComboBox.xaml @@ -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"> 16,8 0 + 0 + 0 - + - - - - - - - - - + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/DescParamComboBox.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Control/DescParamComboBox.xaml.cs index 9f76e5aa..9a33c89c 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/DescParamComboBox.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/DescParamComboBox.xaml.cs @@ -30,8 +30,16 @@ internal sealed partial class DescParamComboBox : UserControl { if (args.NewValue != args.OldValue && args.NewValue is List> list) { - descParamComboBox.SelectedItem = list.ElementAtOrLastOrDefault(descParamComboBox.PreferredSelectedIndex); + LevelParameters? 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)((ComboBox)sender).SelectedItem; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml index 709ee786..ada71d01 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml @@ -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 @@ - + - - - + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml.cs index deb26f50..b8f2cf13 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/SkillPivot.xaml.cs @@ -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; /// 技能展柜 /// [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; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml index 3de375ba..7677b0e1 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml @@ -247,13 +247,13 @@ - + + + - + + + + - + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index 8566e7f2..c01951f2 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -171,13 +171,14 @@ - + + + @@ -217,11 +218,12 @@ - + + + @@ -247,13 +249,13 @@ - + + + @@ -290,6 +292,12 @@ HeaderIcon="{shcm:FontIcon Glyph=}"> + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml index 7492285c..cfb744f6 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml @@ -192,19 +192,23 @@ Description="{shcm:ResourceString Name=ViewPageSettingApperanceLanguageDescription}" Header="{shcm:ResourceString Name=ViewPageSettingApperanceLanguageHeader}" HeaderIcon="{shcm:FontIcon Glyph=}"> - + + + - + + + @@ -240,13 +244,14 @@ Content="Alt" IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasAlt, Mode=TwoWay}"/> - + + + - + + +