Compare commits

..

72 Commits

Author SHA1 Message Date
Lightczx
39831b0ae1 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-11-10 16:08:54 +08:00
Lightczx
51c9936018 1.7.17 package 2023-11-10 16:08:35 +08:00
Masterain
3466a98ffb Create MGMT-publish.yml 2023-11-09 23:48:29 -08:00
DismissedLight
a064cc10ee Merge pull request #1085 from DGP-Studio/develop 2023-11-10 15:41:33 +08:00
Masterain
3479a19164 New Crowdin updates (#1078)
Co-authored-by: DismissedLight <1686188646@qq.com>
2023-11-10 15:39:00 +08:00
Lightczx
d4549581c1 fix exception capture 2023-11-10 15:33:11 +08:00
Lightczx
f97bc344d0 fix announcement time incorrectness 2023-11-10 14:20:44 +08:00
Lightczx
26d23fec7f impl #830 in previous commit 2023-11-10 11:39:53 +08:00
Lightczx
7442f7f1ec support UIGF v2.4-preview 2023-11-10 11:37:45 +08:00
DismissedLight
3eb2556393 update gacha info endpoints 2023-11-09 23:39:51 +08:00
DismissedLight
cfff6f39fc adjust server timezone 2023-11-09 23:15:08 +08:00
Lightczx
3005031b39 add basic timezone support for gachaitem 2023-11-09 17:18:56 +08:00
Lightczx
71363f4d8d fix #1081 2023-11-09 15:23:51 +08:00
Lightczx
e833578334 rename jsbridge 2023-11-09 11:51:56 +08:00
Lightczx
d529b3cea6 fix #1079 2023-11-09 11:38:30 +08:00
Lightczx
1c0ce62885 fix gacha item corner radius 2023-11-08 13:34:55 +08:00
DismissedLight
acdf2baa9a improve webviewer & hotkey 2023-11-07 21:02:25 +08:00
DismissedLight
ec007d5d81 add fp to jsbridge 2023-11-07 19:08:48 +08:00
Lightczx
5e734ac689 impl #961 2023-11-07 15:37:53 +08:00
DismissedLight
b0ecd048b6 1.7.16 package 2023-11-06 21:41:04 +08:00
DismissedLight
91010d0d8b Merge pull request #1076 from DGP-Studio/develop 2023-11-06 20:57:23 +08:00
Masterain
fc771eb90a New Crowdin updates (#1063) 2023-11-06 20:56:40 +08:00
DismissedLight
80f2fed722 Merge pull request #1075 from DGP-Studio/develop 2023-11-06 20:56:15 +08:00
DismissedLight
bdb406c451 add copy hint for #1074 2023-11-06 20:53:29 +08:00
DismissedLight
5bc957c6a5 fix spiral abyss crash when using 4.2 metadata 2023-11-06 20:32:29 +08:00
Lightczx
416c6f15a6 partial #1074 2023-11-06 17:07:35 +08:00
Lightczx
9eed633e05 DefaultItemCollectionTransitionProvider 2023-11-06 15:52:50 +08:00
Lightczx
7e30173990 update to 4.2 metadata 2023-11-06 14:57:57 +08:00
Lightczx
2200e2e58e fonticon resources 2023-11-06 14:36:36 +08:00
Lightczx
b8886c5cd3 fix #1072 2023-11-06 13:46:59 +08:00
DismissedLight
43007d8fb4 Merge pull request #1073 from qhy040404/develop 2023-11-06 12:40:53 +08:00
Lightczx
88684bff00 code style 2023-11-06 12:40:19 +08:00
qhy040404
0c7ce7a72f Add files 2023-11-06 12:32:22 +08:00
qhy040404
075d92f754 Set IsEnabled by a new property instead of setting it separately for each SettingsCard 2023-11-06 12:18:05 +08:00
Lightczx
a0cba171cc update feat template 2023-11-06 11:43:57 +08:00
Lightczx
f41185310b adjust wish typename 2023-11-06 11:41:33 +08:00
Lightczx
2a4c93d241 impl #778 all 2023-11-06 11:17:16 +08:00
DismissedLight
c0980fabe8 impl #1071 2023-11-05 16:03:10 +08:00
DismissedLight
f2ba316059 recycle fingerprint 2023-11-04 17:47:45 +08:00
DismissedLight
e4e9dd91f1 impl #1062 2023-11-04 17:21:31 +08:00
DismissedLight
749ef0e138 introducing game service facade 2023-11-04 16:53:08 +08:00
DismissedLight
24086ee4d0 optimize UniformStaggeredColumnLayout 2023-11-03 23:32:52 +08:00
DismissedLight
aeb6962ae4 impl #1015 2023-11-03 22:06:51 +08:00
Lightczx
87e5ede91f impl #1068 2023-11-03 16:20:11 +08:00
Lightczx
91de6d170e add fingerprint fetch & fix #1060 2023-11-03 11:52:52 +08:00
Lightczx
3057673cdb fix #1069 2023-11-03 10:11:47 +08:00
Lightczx
c3ace405ac fix pushpage 2023-11-03 09:26:44 +08:00
DismissedLight
0b48581e65 Merge pull request #1065 from qhy040404/main 2023-11-02 21:16:40 +08:00
DismissedLight
4ab129e4a2 code style 2023-11-02 21:14:39 +08:00
qhy040404
13ad36f5b4 Added a master switch for launchOptions 2023-11-02 18:43:07 +08:00
Lightczx
f026321aa8 1.7.15 package 2023-11-02 15:27:40 +08:00
DismissedLight
d1dfdf107b Merge pull request #1064 from DGP-Studio/develop 2023-11-02 15:10:51 +08:00
Lightczx
59f8895675 MarqueeText 2023-11-02 15:05:06 +08:00
Lightczx
4cb3d5f03f launch scheme renewed 2023-11-02 14:23:34 +08:00
Lightczx
067c7d7c4d fix ci 2023-11-02 12:45:11 +08:00
Lightczx
1cc072ba28 EmailSmtpAddress 2023-11-02 11:35:31 +08:00
Lightczx
0e7afa8efb clear username & password after cancel registration 2023-11-02 11:30:00 +08:00
Lightczx
b753728b7e verify code request set token 2023-11-02 11:12:26 +08:00
Lightczx
df019da891 complete cancel registration 2023-11-02 10:42:55 +08:00
Lightczx
c6435f30eb add verify for cancel registration 2023-11-02 10:26:27 +08:00
Lightczx
3ac0be4220 fix unregister passport 2023-11-02 10:07:20 +08:00
Lightczx
24f6a33256 clear username & password after logout 2023-11-02 09:48:22 +08:00
Lightczx
dc9278eb4f fix verifycode crash 2023-11-02 09:42:08 +08:00
Lightczx
4b2c82db62 fix xaml parsing failed 2023-11-02 09:06:56 +08:00
Lightczx
70f30edd7c xaml style rework 2023-11-01 17:03:00 +08:00
Lightczx
c8e8213df6 code style 2023-11-01 15:56:22 +08:00
Lightczx
7cad996902 process cmdline #1061 2023-11-01 15:28:44 +08:00
Lightczx
eec47b72c7 fix #1061 2023-11-01 15:26:36 +08:00
Lightczx
5943b1a1fb impl #886 2023-11-01 13:46:50 +08:00
Lightczx
9f9a5670bc fix dailynote webhook error 2023-11-01 10:45:47 +08:00
Lightczx
10ba927136 fix #1059 2023-10-31 16:45:24 +08:00
Lightczx
07d42cedd1 page style 2023-10-31 15:19:25 +08:00
199 changed files with 6272 additions and 3615 deletions

View File

@@ -1,7 +1,7 @@
name: 功能请求
description: 通过这个议题来向开发团队分享你的想法
title: "[Feat]: 在这里填写一个合适的标题"
labels: ["功能", "priority:none"]
labels: ["功能", "needs-triage", "priority:none"]
assignees:
- Lightczx
body:

View File

@@ -1,7 +1,7 @@
name: Feature Request [English Form]
description: Tell us about your thought
title: "[Feat]: Place your title here"
labels: ["功能", "priority:none"]
labels: ["功能", "needs-triage", "priority:none"]
assignees:
- Lightczx
body:

55
.github/ISSUE_TEMPLATE/MGMT-publish.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Publish Process
description: FOR ADMIN USE ONLY. WILL CAUSE A BAN IF NO PERMISSION.
title: "[Publish]: Version 1.9.98"
labels: ["Publish"]
assignees:
- Lightczx
body:
- type: textarea
id: main-body
attributes:
label: Publish Process
value: |
## 创建版本
- [ ] 同步一次 [Crowdin](https://crowdin.com/project/snap-hutao) 翻译
- [ ] 发布 RC 版本Optional
- [ ] 合并入主分支
- [ ] 整理更新内容,等待翻译
- [ ] 打包
- [ ] 提交微软商店
- [ ] 包含更新日志
- [ ] 在 [Snap.Hutao.Docs@next-patch](https://github.com/DGP-Studio/Snap.Hutao.Docs/tree/next-patch) 分支更新文档并直接开 PR
- [ ] 更新日志
- [ ] 功能文档更新
## 发布版本
- [ ] 在 https://store.rg-adguard.net/ 下载新版本安装包
- [ ] Store URL: https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52
- [ ] 命名格式为 `Snap.Hutao x.x.x.msix`
- [ ] Merge 文档 PR
- [ ] 发布 Release
- [ ] 更新日志格式(以 1.6.2 版本为例)
```jsx
## Update log
https://hut.ao/en/statements/update-log.html#_1-6-2
## 更新日志
[此处从文档复制]
## What's Changed
**Full Changelog**: https://github.com/DGP-Studio/Snap.Hutao/compare/1.6.0...1.6.2
```
- [ ] 通知用户
- type: checkboxes
id: checklist-final
attributes:
label: Final Check
description: Understand what you are doing
options:
- label: I understand that I will get banned from repository if I don't have permission to use this template
required: true

View File

@@ -12,6 +12,7 @@
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.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"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/FontStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Glyph.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/InfoBarOverride.xaml"/>

View File

@@ -0,0 +1,46 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Labs.WinUI.MarqueeTextRns;
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml.Input;
namespace Snap.Hutao.Control.Behavior;
internal sealed class MarqueeTextBehavior : BehaviorBase<MarqueeText>
{
private readonly PointerEventHandler pointerEnteredEventHandler;
private readonly PointerEventHandler pointerExitedEventHandler;
public MarqueeTextBehavior()
{
pointerEnteredEventHandler = OnPointerEntered;
pointerExitedEventHandler = OnPointerExited;
}
protected override bool Initialize()
{
AssociatedObject.PointerEntered += pointerEnteredEventHandler;
AssociatedObject.PointerExited += pointerExitedEventHandler;
return true;
}
protected override bool Uninitialize()
{
AssociatedObject.PointerEntered -= pointerEnteredEventHandler;
AssociatedObject.PointerExited -= pointerExitedEventHandler;
return true;
}
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
{
AssociatedObject.StartMarquee();
}
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
{
AssociatedObject.StopMarquee();
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Animations;
using Microsoft.UI.Xaml;
using Microsoft.Xaml.Interactivity;
namespace Snap.Hutao.Control.Behavior;
[DependencyProperty("Animation", typeof(AnimationSet))]
[DependencyProperty("TargetObject", typeof(UIElement))]
internal sealed partial class StartAnimationActionNoThrow : DependencyObject, IAction
{
/// <inheritdoc/>
public object Execute(object sender, object parameter)
{
if (Animation is not null)
{
if (TargetObject is not null)
{
Animation.Start(TargetObject);
}
else
{
Animation.Start(sender as UIElement);
}
}
return default!;
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("IsItemsEnabled", typeof(bool), true, nameof(OnIsItemsEnabledChanged), IsAttached = true, AttachedType = typeof(SettingsExpander))]
public sealed partial class SettingsExpanderHelper
{
private static void OnIsItemsEnabledChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
foreach (object item in ((SettingsExpander)dp).Items)
{
if (item is Microsoft.UI.Xaml.Controls.Control control)
{
control.IsEnabled = (bool)e.NewValue;
}
}
}
}

View File

@@ -0,0 +1,151 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using System.Numerics;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
internal sealed class DefaultItemCollectionTransitionProvider : ItemCollectionTransitionProvider
{
private const double DefaultAnimationDurationInMs = 300.0;
static DefaultItemCollectionTransitionProvider()
{
AnimationSlowdownFactor = 1.0;
}
public static double AnimationSlowdownFactor { get; set; }
protected override bool ShouldAnimateCore(ItemCollectionTransition transition)
{
return true;
}
protected override void StartTransitions(IList<ItemCollectionTransition> transitions)
{
List<ItemCollectionTransition> addTransitions = new();
List<ItemCollectionTransition> removeTransitions = new();
List<ItemCollectionTransition> moveTransitions = new();
foreach (ItemCollectionTransition transition in addTransitions)
{
switch (transition.Operation)
{
case ItemCollectionTransitionOperation.Add:
addTransitions.Add(transition);
break;
case ItemCollectionTransitionOperation.Remove:
removeTransitions.Add(transition);
break;
case ItemCollectionTransitionOperation.Move:
moveTransitions.Add(transition);
break;
}
}
StartAddTransitions(addTransitions, removeTransitions.Count > 0, moveTransitions.Count > 0);
StartRemoveTransitions(removeTransitions);
StartMoveTransitions(moveTransitions, removeTransitions.Count > 0);
}
private static void StartAddTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveTransitions, bool hasMoveTransitions)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation fadeInAnimation = compositor.CreateScalarKeyFrameAnimation();
fadeInAnimation.InsertKeyFrame(0.0f, 0.0f);
if (hasMoveTransitions && hasRemoveTransitions)
{
fadeInAnimation.InsertKeyFrame(0.66f, 0.0f);
}
else if (hasMoveTransitions || hasRemoveTransitions)
{
fadeInAnimation.InsertKeyFrame(0.5f, 0.0f);
}
fadeInAnimation.InsertKeyFrame(1.0f, 1.0f);
fadeInAnimation.Duration = TimeSpan.FromMilliseconds(
DefaultAnimationDurationInMs * ((hasRemoveTransitions ? 1 : 0) + (hasMoveTransitions ? 1 : 0) + 1) * AnimationSlowdownFactor);
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
visual.StartAnimation("Opacity", fadeInAnimation);
batch.End();
batch.Completed += (_, _) => progress.Complete();
}
}
private static void StartRemoveTransitions(IList<ItemCollectionTransition> transitions)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation fadeOutAnimation = compositor.CreateScalarKeyFrameAnimation();
fadeOutAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue");
fadeOutAnimation.InsertKeyFrame(1.0f, 0.0f);
fadeOutAnimation.Duration = TimeSpan.FromMilliseconds(DefaultAnimationDurationInMs * AnimationSlowdownFactor);
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
visual.StartAnimation(nameof(Visual.Opacity), fadeOutAnimation);
batch.End();
batch.Completed += (_, _) =>
{
visual.Opacity = 1.0f;
progress.Complete();
};
}
}
private static void StartMoveTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveAnimations)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
// Animate offset.
if (transition.OldBounds.X != transition.NewBounds.X ||
transition.OldBounds.Y != transition.NewBounds.Y)
{
AnimateOffset(visual, compositor, transition.OldBounds, transition.NewBounds, hasRemoveAnimations);
}
batch.End();
batch.Completed += (_, _) => progress.Complete();
}
}
private static void AnimateOffset(Visual visual, Compositor compositor, Rect oldBounds, Rect newBounds, bool hasRemoveAnimations)
{
Vector2KeyFrameAnimation offsetAnimation = compositor.CreateVector2KeyFrameAnimation();
offsetAnimation.SetVector2Parameter("delta", new Vector2(
(float)(oldBounds.X - newBounds.X),
(float)(oldBounds.Y - newBounds.Y)));
offsetAnimation.SetVector2Parameter("final", default);
offsetAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue + delta");
if (hasRemoveAnimations)
{
offsetAnimation.InsertExpressionKeyFrame(0.5f, "delta");
}
offsetAnimation.InsertExpressionKeyFrame(1.0f, "final");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(
DefaultAnimationDurationInMs * ((hasRemoveAnimations ? 1 : 0) + 1) * AnimationSlowdownFactor);
visual.StartAnimation("TransformMatrix._41_42", offsetAnimation);
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Control.Layout;
[DebuggerDisplay("Count = {Count}, Height = {Height}")]
internal class UniformStaggeredColumnLayout : List<UniformStaggeredItem>
{
public double Height { get; private set; }
public new void Add(UniformStaggeredItem item)
{
Height = item.Top + item.Height;
base.Add(item);
}
public new void Clear()
{
Height = 0;
base.Clear();
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Snap.Hutao.Control.Layout;
internal sealed class UniformStaggeredItem
{
public UniformStaggeredItem(int index)
{
Index = index;
}
public double Top { get; internal set; }
public double Height { get; internal set; }
public int Index { get; }
public UIElement? Element { get; internal set; }
}

View File

@@ -0,0 +1,280 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
[DependencyProperty("MinItemWidth", typeof(double), 0D, nameof(OnMinItemWidthChanged))]
[DependencyProperty("MinColumnSpacing", typeof(double), 0D, nameof(OnSpacingChanged))]
[DependencyProperty("MinRowSpacing", typeof(double), 0D, nameof(OnSpacingChanged))]
internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
{
/// <inheritdoc/>
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = new UniformStaggeredLayoutState(context);
base.InitializeForContextCore(context);
}
/// <inheritdoc/>
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = null;
base.UninitializeForContextCore(context);
}
/// <inheritdoc/>
protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
{
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
state.RemoveFromIndex(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Replace:
state.RemoveFromIndex(args.NewStartingIndex);
state.RecycleElementAt(args.NewStartingIndex); // We must recycle the element to ensure that it gets the correct context
break;
case NotifyCollectionChangedAction.Move:
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
int maxIndex = Math.Max(args.NewStartingIndex, args.OldStartingIndex);
state.RemoveRange(minIndex, maxIndex);
break;
case NotifyCollectionChangedAction.Remove:
state.RemoveFromIndex(args.OldStartingIndex);
break;
case NotifyCollectionChangedAction.Reset:
state.Clear();
break;
}
base.OnItemsChangedCore(context, source, args);
}
/// <inheritdoc/>
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
if (context.ItemCount == 0)
{
return new Size(availableSize.Width, 0);
}
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
{
return new Size(availableSize.Width, 0.0f);
}
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
double availableWidth = availableSize.Width;
double availableHeight = availableSize.Height;
(int numberOfColumns, double columnWidth) = GetNumberOfColumnsAndWidth(availableWidth, MinItemWidth, MinColumnSpacing);
if (columnWidth != state.ColumnWidth)
{
// The items will need to be remeasured
state.Clear();
}
state.ColumnWidth = columnWidth;
// adjust for column spacing on all columns expect the first
double totalWidth = state.ColumnWidth + ((numberOfColumns - 1) * (state.ColumnWidth + MinColumnSpacing));
if (totalWidth > availableWidth)
{
numberOfColumns--;
}
else if (double.IsInfinity(availableWidth))
{
availableWidth = totalWidth;
}
if (numberOfColumns != state.NumberOfColumns)
{
// The items will not need to be remeasured, but they will need to go into new columns
state.ClearColumns();
}
if (MinRowSpacing != state.RowSpacing)
{
// If the RowSpacing changes the height of the rows will be different.
// The columns stores the height so we'll want to clear them out to
// get the proper height
state.ClearColumns();
state.RowSpacing = MinRowSpacing;
}
Span<double> columnHeights = new double[numberOfColumns];
Span<int> itemsPerColumn = new int[numberOfColumns];
HashSet<int> deadColumns = new();
for (int i = 0; i < context.ItemCount; i++)
{
int columnIndex = GetLowestColumnIndex(columnHeights);
bool measured = false;
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
ElementRealizationOptions options = i == 0 ? ElementRealizationOptions.ForceCreate : ElementRealizationOptions.None;
// Item has not been measured yet. Get the element and store the values
UIElement element = context.GetOrCreateElementAt(i, options);
element.Measure(new Size(state.ColumnWidth, availableHeight));
item.Height = element.DesiredSize.Height;
item.Element = element;
measured = true;
}
double spacing = itemsPerColumn[columnIndex] > 0 ? MinRowSpacing : 0;
item.Top = columnHeights[columnIndex] + spacing;
double bottom = item.Top + item.Height;
columnHeights[columnIndex] = bottom;
itemsPerColumn[columnIndex]++;
state.AddItemToColumn(item, columnIndex);
if (bottom < context.RealizationRect.Top)
{
// The bottom of the element is above the realization area
if (item.Element is not null)
{
context.RecycleElement(item.Element);
item.Element = null;
}
}
else if (item.Top > context.RealizationRect.Bottom)
{
// The top of the element is below the realization area
if (item.Element is not null)
{
context.RecycleElement(item.Element);
item.Element = null;
}
deadColumns.Add(columnIndex);
}
else if (measured == false)
{
// We ALWAYS want to measure an item that will be in the bounds
item.Element = context.GetOrCreateElementAt(i);
item.Element.Measure(new Size(state.ColumnWidth, availableHeight));
if (item.Height != item.Element.DesiredSize.Height)
{
// this item changed size; we need to recalculate layout for everything after this
state.RemoveFromIndex(i + 1);
item.Height = item.Element.DesiredSize.Height;
columnHeights[columnIndex] = item.Top + item.Height;
}
}
if (deadColumns.Count == numberOfColumns)
{
break;
}
}
double desiredHeight = state.GetHeight();
return new Size(availableWidth, desiredHeight);
}
/// <inheritdoc/>
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
{
return finalSize;
}
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
// Cycle through each column and arrange the items that are within the realization bounds
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++)
{
ref readonly UniformStaggeredItem item = ref layoutSpan[i];
double bottom = item.Top + item.Height;
if (bottom < context.RealizationRect.Top)
{
// element is above the realization bounds
continue;
}
if (item.Top <= context.RealizationRect.Bottom)
{
double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
Rect bounds = new(itemHorizontalOffset, item.Top, state.ColumnWidth, item.Height);
UIElement element = context.GetOrCreateElementAt(item.Index);
element.Arrange(bounds);
}
else
{
break;
}
}
}
return finalSize;
}
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double minColumnSpacing)
{
// test if the width can fit in 2 items
if ((2 * minItemWidth) + minColumnSpacing > availableWidth)
{
return (1, availableWidth);
}
int columnCount = Math.Max(1, (int)((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing)));
double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount;
return (columnCount, columnWidthAddSpacing - minColumnSpacing);
}
private static int GetLowestColumnIndex(in ReadOnlySpan<double> columnHeights)
{
int columnIndex = 0;
double height = columnHeights[0];
for (int j = 1; j < columnHeights.Length; j++)
{
if (columnHeights[j] < height)
{
columnIndex = j;
height = columnHeights[j];
}
}
return columnIndex;
}
private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
panel.InvalidateMeasure();
}
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
panel.InvalidateMeasure();
}
}

View File

@@ -0,0 +1,192 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Layout;
internal sealed class UniformStaggeredLayoutState
{
private readonly List<UniformStaggeredItem> items = new();
private readonly VirtualizingLayoutContext context;
private readonly Dictionary<int, UniformStaggeredColumnLayout> columnLayout = new();
private double lastAverageHeight;
public UniformStaggeredLayoutState(VirtualizingLayoutContext context)
{
this.context = context;
}
public double ColumnWidth { get; internal set; }
public int NumberOfColumns
{
get => columnLayout.Count;
}
public double RowSpacing { get; internal set; }
internal void AddItemToColumn(UniformStaggeredItem item, int columnIndex)
{
if (!this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout))
{
columnLayout = new();
this.columnLayout[columnIndex] = columnLayout;
}
if (!columnLayout.Contains(item))
{
columnLayout.Add(item);
}
}
[SuppressMessage("", "CA2201")]
internal UniformStaggeredItem GetItemAt(int index)
{
if (index < 0)
{
throw new IndexOutOfRangeException();
}
if (index <= (items.Count - 1))
{
return items[index];
}
else
{
UniformStaggeredItem item = new(index);
items.Add(item);
return item;
}
}
[SuppressMessage("", "SH007")]
internal UniformStaggeredColumnLayout GetColumnLayout(int columnIndex)
{
this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout);
return columnLayout!;
}
/// <summary>
/// Clear everything that has been calculated.
/// </summary>
internal void Clear()
{
columnLayout.Clear();
items.Clear();
}
/// <summary>
/// Clear the layout columns so they will be recalculated.
/// </summary>
internal void ClearColumns()
{
columnLayout.Clear();
}
/// <summary>
/// Gets the estimated height of the layout.
/// </summary>
/// <returns>The estimated height of the layout.</returns>
/// <remarks>
/// If all of the items have been calculated then the actual height will be returned.
/// If all of the items have not been calculated then an estimated height will be calculated based on the average height of the items.
/// </remarks>
internal double GetHeight()
{
double desiredHeight = columnLayout.Values.Max(c => c.Height);
int itemCount = columnLayout.Values.Sum(c => c.Count);
if (itemCount == context.ItemCount)
{
return desiredHeight;
}
double averageHeight = 0;
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
{
averageHeight += layout.Height / layout.Count;
}
averageHeight /= columnLayout.Count;
double estimatedHeight = (averageHeight * context.ItemCount) / columnLayout.Count;
if (estimatedHeight > desiredHeight)
{
desiredHeight = estimatedHeight;
}
if (Math.Abs(desiredHeight - lastAverageHeight) < 5)
{
return lastAverageHeight;
}
lastAverageHeight = desiredHeight;
return desiredHeight;
}
internal void RecycleElementAt(int index)
{
UIElement element = context.GetOrCreateElementAt(index);
context.RecycleElement(element);
}
internal void RemoveFromIndex(int index)
{
if (index >= items.Count)
{
// Item was added/removed but we haven't realized that far yet
return;
}
int numToRemove = items.Count - index;
items.RemoveRange(index, numToRemove);
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
{
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
for (int i = 0; i < layoutSpan.Length; i++)
{
if (layoutSpan[i].Index >= index)
{
numToRemove = layoutSpan.Length - i;
layout.RemoveRange(i, numToRemove);
break;
}
}
}
}
internal void RemoveRange(int startIndex, int endIndex)
{
for (int i = startIndex; i <= endIndex; i++)
{
if (i > items.Count)
{
break;
}
ref readonly UniformStaggeredItem item = ref CollectionsMarshal.AsSpan(items)[i];
item.Height = 0;
item.Top = 0;
// We must recycle all elements to ensure that it gets the correct context
RecycleElementAt(i);
}
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
{
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
for (int i = 0; i < layoutSpan.Length; i++)
{
if ((startIndex <= layoutSpan[i].Index) && (layoutSpan[i].Index <= endIndex))
{
int numToRemove = layoutSpan.Length - i;
layout.RemoveRange(i, numToRemove);
break;
}
}
}
}
}

View File

@@ -9,11 +9,11 @@
mc:Ignorable="d">
<cwc:SegmentedItem
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}"
Tag="List"
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
<cwc:SegmentedItem
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentGridView}}"
Tag="Grid"
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>

View File

@@ -3,6 +3,7 @@
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Setting;
namespace Snap.Hutao.Control.Panel;
@@ -11,6 +12,8 @@ namespace Snap.Hutao.Control.Panel;
/// </summary>
[HighQuality]
[DependencyProperty("Current", typeof(string), List)]
[DependencyProperty("LocalSettingKeySuffixForCurrent", typeof(string))]
[DependencyProperty("LocalSettingKeyExtraForCurrent", typeof(string), "")]
internal sealed partial class PanelSelector : Segmented
{
public const string List = nameof(List);
@@ -42,21 +45,41 @@ internal sealed partial class PanelSelector : Segmented
selectedIndexChangedCallbackToken = RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged);
}
private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp)
{
Current = IndexTypeMap[(int)GetValue(dp)];
}
private void OnRootLoaded(object sender, RoutedEventArgs e)
private static void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp)
{
PanelSelector selector = (PanelSelector)sender;
selector.SelectedItem = selector.Items.Cast<SegmentedItem>().Single(item => (string)item.Tag == Current);
selector.Current = IndexTypeMap[(int)selector.GetValue(dp)];
if (!string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent))
{
LocalSetting.Set(GetSettingKey(selector), selector.Current);
}
}
private void OnRootUnload(object sender, RoutedEventArgs e)
private static void OnRootLoaded(object sender, RoutedEventArgs e)
{
UnregisterPropertyChangedCallback(SelectedIndexProperty, selectedIndexChangedCallbackToken);
Loaded -= loadedEventHandler;
Unloaded -= unloadedEventHandler;
PanelSelector selector = (PanelSelector)sender;
if (string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent))
{
return;
}
string value = LocalSetting.Get(GetSettingKey(selector), selector.Current);
selector.Current = value;
selector.SelectedItem = selector.Items.Cast<SegmentedItem>().Single(item => (string)item.Tag == selector.Current);
}
private static void OnRootUnload(object sender, RoutedEventArgs e)
{
PanelSelector selector = (PanelSelector)sender;
selector.UnregisterPropertyChangedCallback(SelectedIndexProperty, selector.selectedIndexChangedCallbackToken);
selector.Unloaded -= selector.unloadedEventHandler;
}
private static string GetSettingKey(PanelSelector selector)
{
return $"Control.PanelSelector.{selector.LocalSettingKeySuffixForCurrent}{selector.LocalSettingKeyExtraForCurrent}";
}
}

View File

@@ -1,4 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<CornerRadius x:Key="ControlCornerRadiusTop">4,4,0,0</CornerRadius>
<CornerRadius x:Key="ControlCornerRadiusBottom">0,0,4,4</CornerRadius>
<CornerRadius x:Key="ControlCornerRadiusTopRightAndBottomLeft">0,4,0,4</CornerRadius>
</ResourceDictionary>

View File

@@ -0,0 +1,21 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style
x:Key="WebViewerFlyoutPresenterStyle"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
</Style>
<Style
x:Key="FlyoutPresenterPadding0And2Style"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="0,2"/>
</Style>
<Style
x:Key="FlyoutPresenterPadding6Style"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="6"/>
</Style>
</ResourceDictionary>

View File

@@ -3,8 +3,18 @@
<x:String x:Key="FontIconContentSetting">&#xE713;</x:String>
<x:String x:Key="FontIconContentRefresh">&#xE72C;</x:String>
<x:String x:Key="FontIconContentDelete">&#xE74D;</x:String>
<x:String x:Key="FontIconContentChevronRight">&#xE76C;</x:String>
<x:String x:Key="FontIconContentWarning">&#xE7BA;</x:String>
<x:String x:Key="FontIconContentGame">&#xE7FC;</x:String>
<x:String x:Key="FontIconContentOpenInNewWindow">&#xE8A7;</x:String>
<x:String x:Key="FontIconContentFolder">&#xE8B7;</x:String>
<x:String x:Key="FontIconContentCopy">&#xE8C8;</x:String>
<x:String x:Key="FontIconContentBulletedList">&#xE8FD;</x:String>
<x:String x:Key="FontIconContentCheckList">&#xE9D5;</x:String>
<x:String x:Key="FontIconContentWebsite">&#xEB41;</x:String>
<x:String x:Key="FontIconContentHomeGroup">&#xEC26;</x:String>
<x:String x:Key="FontIconContentAsteriskBadge12">&#xEDAD;</x:String>
<x:String x:Key="FontIconContentZipFolder">&#xF012;</x:String>
</ResourceDictionary>
<x:String x:Key="FontIconContentGridView">&#xF0E2;</x:String>
<x:String x:Key="FontIconContentGiftboxOpen">&#xF133;</x:String>
</ResourceDictionary>

View File

@@ -11,13 +11,28 @@
<ItemsPanelTemplate x:Key="WrapPanelSpacing4Template">
<cwcont:WrapPanel HorizontalSpacing="4" VerticalSpacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalStackPanelTemplate">
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing0Template">
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing2Template">
<StackPanel Orientation="Horizontal" Spacing="2"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
<cwcont:UniformGrid
ColumnSpacing="2"
Columns="2"
RowSpacing="2"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="UniformGridColumns5Spacing4Template">
<cwcont:UniformGrid
ColumnSpacing="4"
Columns="5"
RowSpacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="UniformGridColumns5Spacing8Template">
<cwcont:UniformGrid
ColumnSpacing="8"
Columns="5"
RowSpacing="8"/>
</ItemsPanelTemplate>
</ResourceDictionary>

View File

@@ -14,4 +14,10 @@
<TransitionCollection x:Key="ReorderThemeTransitions">
<ReorderThemeTransition/>
</TransitionCollection>
<TransitionCollection x:Key="RepositionThemeTransitions">
<RepositionThemeTransition/>
</TransitionCollection>
<TransitionCollection x:Key="NavigationThemeTransitions">
<NavigationThemeTransition/>
</TransitionCollection>
</ResourceDictionary>

View File

@@ -15,22 +15,7 @@ internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
/// <inheritdoc/>
public object? Convert(object value, Type targetType, object parameter, string language)
{
#if DEBUG
try
{
return Convert((TFrom)value);
}
catch (Exception ex)
{
Ioc.Default
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
.LogError(ex, "值转换器异常");
throw;
}
#else
return Convert((TFrom)value);
#endif
}
/// <inheritdoc/>

View File

@@ -36,7 +36,7 @@ internal static partial class IocHttpClientConfiguration
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// 对于需要添加动态密钥1的客户端使用此配置
/// </summary>
/// <param name="client">配置后的客户端</param>
private static void XRpcConfiguration(HttpClient client)
@@ -50,7 +50,7 @@ internal static partial class IocHttpClientConfiguration
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// 对于需要添加动态密钥2的客户端使用此配置
/// </summary>
/// <param name="client">配置后的客户端</param>
private static void XRpc2Configuration(HttpClient client)
@@ -64,11 +64,11 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// 对于需要添加动态密钥1的客户端使用此配置
/// HoYoLAB app
/// </summary>
/// <param name="client">配置后的客户端</param>
@@ -84,7 +84,7 @@ internal static partial class IocHttpClientConfiguration
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// 对于需要添加动态密钥2的客户端使用此配置
/// HoYoLAB web
/// </summary>
/// <param name="client">配置后的客户端</param>

View File

@@ -7,6 +7,8 @@ namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 实现日期的转换
/// 此转换器无法实现无损往返
/// 必须在反序列化后调整 Offset
/// </summary>
[HighQuality]
internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
@@ -18,7 +20,10 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
if (reader.GetString() is { } dataTimeString)
{
return DateTimeOffset.ParseExact(dataTimeString, Format, CultureInfo.CurrentCulture);
// By doing so, the DateTimeOffset parsed out will be a
// no offset datetime, and need to be adjusted later
DateTime dateTime = DateTime.ParseExact(dataTimeString, Format, CultureInfo.InvariantCulture);
return new DateTimeOffset(dateTime, default);
}
return default;
@@ -27,6 +32,6 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format, CultureInfo.CurrentCulture));
writer.WriteStringValue(value.DateTime.ToString(Format, CultureInfo.InvariantCulture));
}
}

View File

@@ -163,7 +163,7 @@ internal sealed partial class Activation : IActivation
{
await taskContext.SwitchToMainThreadAsync();
currentWindowReference.Window = serviceProvider.GetRequiredService<MainWindow>();
serviceProvider.GetRequiredService<MainWindow>();
serviceProvider
.GetRequiredService<IMetadataService>()
@@ -270,7 +270,7 @@ internal sealed partial class Activation : IActivation
if (currentWindowReference.Window is null)
{
currentWindowReference.Window = serviceProvider.GetRequiredService<LaunchGameWindow>();
serviceProvider.GetRequiredService<LaunchGameWindow>();
}
else
{

View File

@@ -2,6 +2,9 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Windows.Win32.Foundation;
using WinRT.Interop;
namespace Snap.Hutao.Core.LifeCycle;
@@ -11,4 +14,11 @@ internal static class CurrentWindowReferenceExtension
{
return reference.Window.Content.XamlRoot;
}
public static HWND GetWindowHandle(this ICurrentWindowReference reference)
{
return reference.Window is IWindowOptionsSource optionsSource
? optionsSource.WindowOptions.Hwnd
: (HWND)WindowNative.GetWindowHandle(reference.Window);
}
}

View File

@@ -7,5 +7,8 @@ namespace Snap.Hutao.Core.LifeCycle;
internal interface ICurrentWindowReference
{
/// <summary>
/// Only set in WindowController
/// </summary>
public Window Window { get; set; }
}

View File

@@ -81,4 +81,6 @@ internal static class SettingKeys
public const string IsHomeCardGachaStatisticsPresented = "IsHomeCardGachaStatisticsPresented";
public const string IsHomeCardAchievementPresented = "IsHomeCardAchievementPresented";
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Dispatching;
namespace Snap.Hutao.Core.Threading;
internal sealed class DispatcherQueueSynchronizationContextSendSupport : SynchronizationContext
{
private readonly DispatcherQueue dispatcherQueue;
public DispatcherQueueSynchronizationContextSendSupport(DispatcherQueue dispatcherQueue)
{
this.dispatcherQueue = dispatcherQueue;
}
public override void Post(SendOrPostCallback d, object? state)
{
ArgumentNullException.ThrowIfNull(d);
dispatcherQueue.TryEnqueue(() => d(state));
}
public override void Send(SendOrPostCallback d, object? state)
{
ArgumentNullException.ThrowIfNull(d);
dispatcherQueue.Invoke(() => d(state));
}
public override SynchronizationContext CreateCopy()
{
return new DispatcherQueueSynchronizationContextSendSupport(dispatcherQueue);
}
}

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Threading;
/// </summary>
internal interface ITaskContext
{
IProgress<T> CreateProgressForMainThread<T>(Action<T> handler);
SynchronizationContext GetSynchronizationContext();
/// <summary>
/// 在主线程上同步等待执行操作

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.Threading;
[Injection(InjectAs.Singleton, typeof(ITaskContext))]
internal sealed class TaskContext : ITaskContext
{
private readonly DispatcherQueueSynchronizationContextSendSupport synchronizationContext;
private readonly DispatcherQueueSynchronizationContext synchronizationContext;
private readonly DispatcherQueue dispatcherQueue;
/// <summary>
@@ -42,8 +42,8 @@ internal sealed class TaskContext : ITaskContext
dispatcherQueue.Invoke(action);
}
public IProgress<T> CreateProgressForMainThread<T>(Action<T> handler)
public SynchronizationContext GetSynchronizationContext()
{
return new DispatcherQueueProgress<T>(handler, synchronizationContext);
return synchronizationContext;
}
}

View File

@@ -0,0 +1,306 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Model;
using System.Text;
using Windows.System;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Windowing.HotKey;
[SuppressMessage("", "SA1124")]
internal sealed class HotKeyCombination : ObservableObject
{
private readonly ICurrentWindowReference currentWindowReference;
private readonly RuntimeOptions runtimeOptions;
private readonly string settingKey;
private readonly int hotKeyId;
private readonly HotKeyParameter defaultHotKeyParameter;
private bool registered;
private bool modifierHasWindows;
private bool modifierHasControl;
private bool modifierHasShift;
private bool modifierHasAlt;
private NameValue<VirtualKey> keyNameValue;
private HOT_KEY_MODIFIERS modifiers;
private VirtualKey key;
private bool isEnabled;
public HotKeyCombination(IServiceProvider serviceProvider, string settingKey, int hotKeyId, HOT_KEY_MODIFIERS defaultModifiers, VirtualKey defaultKey)
{
currentWindowReference = serviceProvider.GetRequiredService<ICurrentWindowReference>();
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
this.settingKey = settingKey;
this.hotKeyId = hotKeyId;
defaultHotKeyParameter = new(defaultModifiers, defaultKey);
// Initialize Property backing fields
{
// Retrieve from LocalSetting
isEnabled = LocalSetting.Get($"{settingKey}.IsEnabled", true);
HotKeyParameter actual = LocalSettingGetHotKeyParameter();
modifiers = actual.Modifiers;
InitializeModifiersComposeFields();
key = actual.Key;
keyNameValue = VirtualKeys.GetList().Single(v => v.Value == key);
}
}
#region Binding Property
public bool ModifierHasWindows
{
get => modifierHasWindows;
set
{
if (SetProperty(ref modifierHasWindows, value))
{
UpdateModifiers();
}
}
}
public bool ModifierHasControl
{
get => modifierHasControl;
set
{
if (SetProperty(ref modifierHasControl, value))
{
UpdateModifiers();
}
}
}
public bool ModifierHasShift
{
get => modifierHasShift;
set
{
if (SetProperty(ref modifierHasShift, value))
{
UpdateModifiers();
}
}
}
public bool ModifierHasAlt
{
get => modifierHasAlt;
set
{
if (SetProperty(ref modifierHasAlt, value))
{
UpdateModifiers();
}
}
}
public NameValue<VirtualKey> KeyNameValue
{
get => keyNameValue;
set
{
if (value is null)
{
return;
}
if (SetProperty(ref keyNameValue, value))
{
Key = value.Value;
}
}
}
#endregion
public HOT_KEY_MODIFIERS Modifiers
{
get => modifiers;
private set
{
if (SetProperty(ref modifiers, value))
{
OnPropertyChanged(nameof(DisplayName));
LocalSettingSetHotKeyParameterAndRefresh();
}
}
}
public VirtualKey Key
{
get => key;
private set
{
if (SetProperty(ref key, value))
{
OnPropertyChanged(nameof(DisplayName));
LocalSettingSetHotKeyParameterAndRefresh();
}
}
}
public bool IsEnabled
{
get => isEnabled;
set
{
if (SetProperty(ref isEnabled, value))
{
LocalSetting.Set($"{settingKey}.IsEnabled", value);
_ = (value, registered) switch
{
(true, false) => RegisterForCurrentWindow(),
(false, true) => UnregisterForCurrentWindow(),
_ => false,
};
}
}
}
public string DisplayName { get => ToString(); }
public bool RegisterForCurrentWindow()
{
if (!runtimeOptions.IsElevated || !IsEnabled)
{
return false;
}
if (registered)
{
return true;
}
HWND hwnd = currentWindowReference.GetWindowHandle();
BOOL result = RegisterHotKey(hwnd, hotKeyId, Modifiers, (uint)Key);
registered = result;
return result;
}
public bool UnregisterForCurrentWindow()
{
if (!runtimeOptions.IsElevated)
{
return false;
}
if (!registered)
{
return true;
}
HWND hwnd = currentWindowReference.GetWindowHandle();
BOOL result = UnregisterHotKey(hwnd, hotKeyId);
registered = !result;
return result;
}
public override string ToString()
{
StringBuilder stringBuilder = new();
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_WIN))
{
stringBuilder.Append("Win").Append(" + ");
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_CONTROL))
{
stringBuilder.Append("Ctrl").Append(" + ");
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_SHIFT))
{
stringBuilder.Append("Shift").Append(" + ");
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_ALT))
{
stringBuilder.Append("Alt").Append(" + ");
}
stringBuilder.Append(Key);
return stringBuilder.ToString();
}
private void UpdateModifiers()
{
HOT_KEY_MODIFIERS modifiers = default;
if (ModifierHasWindows)
{
modifiers |= HOT_KEY_MODIFIERS.MOD_WIN;
}
if (ModifierHasControl)
{
modifiers |= HOT_KEY_MODIFIERS.MOD_CONTROL;
}
if (ModifierHasShift)
{
modifiers |= HOT_KEY_MODIFIERS.MOD_SHIFT;
}
if (ModifierHasAlt)
{
modifiers |= HOT_KEY_MODIFIERS.MOD_ALT;
}
Modifiers = modifiers;
}
private void InitializeModifiersComposeFields()
{
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_WIN))
{
modifierHasWindows = true;
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_CONTROL))
{
modifierHasControl = true;
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_SHIFT))
{
modifierHasShift = true;
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_ALT))
{
modifierHasAlt = true;
}
}
private unsafe HotKeyParameter LocalSettingGetHotKeyParameter()
{
fixed (HotKeyParameter* pDefaultHotKey = &defaultHotKeyParameter)
{
int value = LocalSetting.Get(settingKey, *(int*)pDefaultHotKey);
return *(HotKeyParameter*)&value;
}
}
private unsafe void LocalSettingSetHotKeyParameterAndRefresh()
{
HotKeyParameter current = new(Modifiers, Key);
LocalSetting.Set(settingKey, *(int*)&current);
UnregisterForCurrentWindow();
RegisterForCurrentWindow();
}
}

View File

@@ -2,69 +2,38 @@
// Licensed under the MIT license.
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Windowing.HotKey;
[SuppressMessage("", "CA1001")]
internal sealed class HotKeyController : IHotKeyController
[ConstructorGenerated]
internal sealed partial class HotKeyController : IHotKeyController
{
private const int DefaultId = 100000;
private static readonly WaitCallback RunMouseClickRepeatForever = MouseClickRepeatForever;
private readonly object locker = new();
private readonly WaitCallback runMouseClickRepeatForever;
private readonly HotKeyOptions hotKeyOptions;
private readonly RuntimeOptions runtimeOptions;
private volatile CancellationTokenSource? cancellationTokenSource;
public HotKeyController(IServiceProvider serviceProvider)
public void RegisterAll()
{
hotKeyOptions = serviceProvider.GetRequiredService<HotKeyOptions>();
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
runMouseClickRepeatForever = MouseClickRepeatForever;
hotKeyOptions.MouseClickRepeatForeverKeyCombination.RegisterForCurrentWindow();
}
public bool Register(in HWND hwnd)
public void UnregisterAll()
{
if (runtimeOptions.IsElevated)
{
return RegisterHotKey(hwnd, DefaultId, default, (uint)VIRTUAL_KEY.VK_F8);
}
return false;
}
public bool Unregister(in HWND hwnd)
{
if (runtimeOptions.IsElevated)
{
return UnregisterHotKey(hwnd, DefaultId);
}
return false;
hotKeyOptions.MouseClickRepeatForeverKeyCombination.UnregisterForCurrentWindow();
}
public void OnHotKeyPressed(in HotKeyParameter parameter)
{
if (parameter is { Key: VIRTUAL_KEY.VK_F8, NativeModifier: 0 })
if (parameter.Equals(hotKeyOptions.MouseClickRepeatForeverKeyCombination))
{
lock (locker)
{
if (hotKeyOptions.IsMouseClickRepeatForeverOn)
{
cancellationTokenSource?.Cancel();
cancellationTokenSource = default;
hotKeyOptions.IsMouseClickRepeatForeverOn = false;
}
else
{
cancellationTokenSource = new();
ThreadPool.QueueUserWorkItem(runMouseClickRepeatForever, cancellationTokenSource.Token);
hotKeyOptions.IsMouseClickRepeatForeverOn = true;
}
}
ToggleMouseClickRepeatForever();
}
}
@@ -76,7 +45,7 @@ internal sealed class HotKeyController : IHotKeyController
}
[SuppressMessage("", "SH007")]
private unsafe void MouseClickRepeatForever(object? state)
private static unsafe void MouseClickRepeatForever(object? state)
{
CancellationToken token = (CancellationToken)state!;
@@ -102,4 +71,25 @@ internal sealed class HotKeyController : IHotKeyController
Thread.Sleep(Random.Shared.Next(100, 150));
}
}
private void ToggleMouseClickRepeatForever()
{
lock (locker)
{
if (hotKeyOptions.IsMouseClickRepeatForeverOn)
{
// Turn off
cancellationTokenSource?.Cancel();
cancellationTokenSource = default;
hotKeyOptions.IsMouseClickRepeatForeverOn = false;
}
else
{
// Turn on
cancellationTokenSource = new();
ThreadPool.QueueUserWorkItem(RunMouseClickRepeatForever, cancellationTokenSource.Token);
hotKeyOptions.IsMouseClickRepeatForeverOn = true;
}
}
}
}

View File

@@ -2,13 +2,34 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Model;
using Windows.System;
namespace Snap.Hutao.Core.Windowing.HotKey;
[Injection(InjectAs.Singleton)]
internal sealed class HotKeyOptions : ObservableObject
internal sealed partial class HotKeyOptions : ObservableObject
{
private bool isVirtualKeyF8Pressed;
private bool isMouseClickRepeatForeverOn;
private HotKeyCombination mouseClickRepeatForeverKeyCombination;
public bool IsMouseClickRepeatForeverOn { get => isVirtualKeyF8Pressed; set => SetProperty(ref isVirtualKeyF8Pressed, value); }
public HotKeyOptions(IServiceProvider serviceProvider)
{
mouseClickRepeatForeverKeyCombination = new(serviceProvider, SettingKeys.HotKeyMouseClickRepeatForever, 100000, default, VirtualKey.F8);
}
public List<NameValue<VirtualKey>> VirtualKeys { get; } = HotKey.VirtualKeys.GetList();
public bool IsMouseClickRepeatForeverOn
{
get => isMouseClickRepeatForeverOn;
set => SetProperty(ref isMouseClickRepeatForeverOn, value);
}
public HotKeyCombination MouseClickRepeatForeverKeyCombination
{
get => mouseClickRepeatForeverKeyCombination;
set => SetProperty(ref mouseClickRepeatForeverKeyCombination, value);
}
}

View File

@@ -1,17 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.System;
using Windows.Win32.UI.Input.KeyboardAndMouse;
namespace Snap.Hutao.Core.Windowing.HotKey;
internal readonly struct HotKeyParameter
/// <summary>
/// HotKeyParameter
/// The size of this struct must be sizeof(LPARAM) or 4
/// </summary>
internal readonly struct HotKeyParameter : IEquatable<HotKeyCombination>
{
public readonly ushort NativeModifier;
public readonly VIRTUAL_KEY Key;
public readonly ushort NativeModifiers;
public readonly VIRTUAL_KEY NativeKey;
public readonly HOT_KEY_MODIFIERS Modifier
public HotKeyParameter(HOT_KEY_MODIFIERS modifiers, VirtualKey key)
{
get => (HOT_KEY_MODIFIERS)NativeModifier;
NativeModifiers = (ushort)modifiers;
NativeKey = (VIRTUAL_KEY)key;
}
public readonly HOT_KEY_MODIFIERS Modifiers
{
get => (HOT_KEY_MODIFIERS)NativeModifiers;
}
public readonly VirtualKey Key
{
get => (VirtualKey)NativeKey;
}
public bool Equals(HotKeyCombination? other)
{
if (other is null)
{
return false;
}
return Modifiers == other.Modifiers && Key == other.Key;
}
}

View File

@@ -1,15 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Win32.Foundation;
namespace Snap.Hutao.Core.Windowing.HotKey;
internal interface IHotKeyController
{
void OnHotKeyPressed(in HotKeyParameter parameter);
bool Register(in HWND hwnd);
void RegisterAll();
bool Unregister(in HWND hwnd);
void UnregisterAll();
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model;
using Windows.System;
namespace Snap.Hutao.Core.Windowing.HotKey;
internal static class VirtualKeys
{
private static readonly List<NameValue<VirtualKey>> Values = CollectionsNameValue.ListFromEnum<VirtualKey>();
public static List<NameValue<VirtualKey>> GetList()
{
return Values;
}
}

View File

@@ -6,6 +6,7 @@ using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service;
using System.IO;
@@ -32,8 +33,9 @@ internal sealed class WindowController
this.options = options;
this.serviceProvider = serviceProvider;
// Window reference must be set before Window Subclass created
serviceProvider.GetRequiredService<ICurrentWindowReference>().Window = window;
subclass = new(window, options, serviceProvider);
InitializeCore();
}
@@ -78,7 +80,7 @@ internal sealed class WindowController
private void RecoverOrInitWindowSize()
{
// Set first launch size
double scale = options.GetWindowScale();
double scale = options.GetRasterizationScale();
SizeInt32 scaledSize = options.InitSize.Scale(scale);
RectInt32 rect = StructMarshal.RectInt32(scaledSize);
@@ -108,14 +110,14 @@ internal sealed class WindowController
// prevent save value when we are maximized.
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
{
double scale = 1 / options.GetWindowScale();
double scale = 1.0 / options.GetRasterizationScale();
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)window.AppWindow.GetRect().Scale(scale));
}
}
private void OnOptionsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(AppOptions.BackdropType))
if (e.PropertyName is nameof(AppOptions.BackdropType))
{
if (sender is AppOptions options)
{
@@ -198,7 +200,7 @@ internal sealed class WindowController
{
AppWindowTitleBar appTitleBar = window.AppWindow.TitleBar;
double scale = options.GetWindowScale();
double scale = options.GetRasterizationScale();
// 48 is the navigation button leftInset
RectInt32 dragRect = StructMarshal.RectInt32(48, 0, options.TitleBar.ActualSize).Scale(scale);

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.Graphics;
@@ -20,6 +21,11 @@ internal readonly struct WindowOptions
/// </summary>
public readonly HWND Hwnd;
/// <summary>
/// 非客户端区域指针源
/// </summary>
public readonly InputNonClientPointerSource InputNonClientPointerSource;
/// <summary>
/// 标题栏元素
/// </summary>
@@ -50,6 +56,7 @@ internal readonly struct WindowOptions
public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, bool persistSize = false)
{
Hwnd = (HWND)WindowNative.GetWindowHandle(window);
InputNonClientPointerSource = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
TitleBar = titleBar;
InitSize = initSize;
PersistSize = persistSize;
@@ -59,7 +66,7 @@ internal readonly struct WindowOptions
/// 获取窗体当前的DPI缩放比
/// </summary>
/// <returns>缩放比</returns>
public double GetWindowScale()
public double GetRasterizationScale()
{
uint dpi = GetDpiForWindow(Hwnd);
return Math.Round(dpi / 96D, 2, MidpointRounding.AwayFromZero);

View File

@@ -45,7 +45,7 @@ internal sealed class WindowSubclass : IDisposable
{
windowProc = OnSubclassProcedure;
bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, 0);
hotKeyController.Register(options.Hwnd);
hotKeyController.RegisterAll();
bool titleBarHooked = true;
@@ -72,7 +72,7 @@ internal sealed class WindowSubclass : IDisposable
/// <inheritdoc/>
public void Dispose()
{
hotKeyController.Unregister(options.Hwnd);
hotKeyController.UnregisterAll();
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
windowProc = null;
@@ -93,7 +93,7 @@ internal sealed class WindowSubclass : IDisposable
{
if (window is IMinMaxInfoHandler handler)
{
handler.HandleMinMaxInfo(ref *(MINMAXINFO*)lParam.Value, options.GetWindowScale());
handler.HandleMinMaxInfo(ref *(MINMAXINFO*)lParam.Value, options.GetRasterizationScale());
}
break;

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
internal struct UnsafeDateTimeOffset
{
private DateTime dateTime;
private short offsetMinutes;
public DateTime DateTime { readonly get => dateTime; set => dateTime = value; }
[SuppressMessage("", "SH002")]
public static unsafe DateTimeOffset AdjustOffsetOnly(DateTimeOffset dateTimeOffset, in TimeSpan offset)
{
UnsafeDateTimeOffset* pUnsafe = (UnsafeDateTimeOffset*)&dateTimeOffset;
pUnsafe->offsetMinutes = (short)(offset.Ticks / TimeSpan.TicksPerMinute);
return dateTimeOffset;
}
}

View File

@@ -3,9 +3,8 @@
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Factory.Abstraction;
namespace Snap.Hutao.Factory;
namespace Snap.Hutao.Factory.ContentDialog;
/// <inheritdoc cref="IContentDialogFactory"/>
[HighQuality]
@@ -13,15 +12,15 @@ namespace Snap.Hutao.Factory;
[Injection(InjectAs.Singleton, typeof(IContentDialogFactory))]
internal sealed partial class ContentDialogFactory : IContentDialogFactory
{
private readonly ICurrentWindowReference currentWindowReference;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly ICurrentWindowReference currentWindowReference;
/// <inheritdoc/>
public async ValueTask<ContentDialogResult> CreateForConfirmAsync(string title, string content)
{
await taskContext.SwitchToMainThreadAsync();
ContentDialog dialog = new()
Microsoft.UI.Xaml.Controls.ContentDialog dialog = new()
{
XamlRoot = currentWindowReference.GetXamlRoot(),
Title = title,
@@ -37,7 +36,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
public async ValueTask<ContentDialogResult> CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
{
await taskContext.SwitchToMainThreadAsync();
ContentDialog dialog = new()
Microsoft.UI.Xaml.Controls.ContentDialog dialog = new()
{
XamlRoot = currentWindowReference.GetXamlRoot(),
Title = title,
@@ -51,10 +50,10 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
}
/// <inheritdoc/>
public async ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title)
public async ValueTask<Microsoft.UI.Xaml.Controls.ContentDialog> CreateForIndeterminateProgressAsync(string title)
{
await taskContext.SwitchToMainThreadAsync();
ContentDialog dialog = new()
Microsoft.UI.Xaml.Controls.ContentDialog dialog = new()
{
XamlRoot = currentWindowReference.GetXamlRoot(),
Title = title,
@@ -65,7 +64,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
}
public async ValueTask<TContentDialog> CreateInstanceAsync<TContentDialog>(params object[] parameters)
where TContentDialog : ContentDialog
where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog
{
await taskContext.SwitchToMainThreadAsync();
TContentDialog contentDialog = serviceProvider.CreateInstance<TContentDialog>(parameters);
@@ -74,7 +73,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
}
public TContentDialog CreateInstance<TContentDialog>(params object[] parameters)
where TContentDialog : ContentDialog
where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog
{
TContentDialog contentDialog = serviceProvider.CreateInstance<TContentDialog>(parameters);
contentDialog.XamlRoot = currentWindowReference.GetXamlRoot();

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Factory.Abstraction;
namespace Snap.Hutao.Factory.ContentDialog;
/// <summary>
/// 内容对话框工厂
@@ -33,11 +33,11 @@ internal interface IContentDialogFactory
/// </summary>
/// <param name="title">标题</param>
/// <returns>内容对话框</returns>
ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title);
ValueTask<Microsoft.UI.Xaml.Controls.ContentDialog> CreateForIndeterminateProgressAsync(string title);
TContentDialog CreateInstance<TContentDialog>(params object[] parameters)
where TContentDialog : ContentDialog;
where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog;
ValueTask<TContentDialog> CreateInstanceAsync<TContentDialog>(params object[] parameters)
where TContentDialog : ContentDialog;
where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog;
}

View File

@@ -3,7 +3,7 @@
using Windows.Storage.Pickers;
namespace Snap.Hutao.Factory.Abstraction;
namespace Snap.Hutao.Factory.Picker;
/// <summary>
/// 文件选择器工厂

View File

@@ -4,11 +4,11 @@
using Snap.Hutao.Core;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Factory.Abstraction;
using Windows.Storage.Pickers;
using Windows.Win32.Foundation;
using WinRT.Interop;
namespace Snap.Hutao.Factory;
namespace Snap.Hutao.Factory.Picker;
/// <inheritdoc cref="IPickerFactory"/>
[HighQuality]
@@ -18,7 +18,7 @@ internal sealed partial class PickerFactory : IPickerFactory
{
private const string AnyType = "*";
private readonly ICurrentWindowReference currentWindow;
private readonly ICurrentWindowReference currentWindowReference;
/// <inheritdoc/>
public FileOpenPicker GetFileOpenPicker(PickerLocationId location, string commitButton, params string[] fileTypes)
@@ -80,10 +80,9 @@ internal sealed partial class PickerFactory : IPickerFactory
{
// Create a folder picker.
T picker = new();
if (currentWindow.Window is IWindowOptionsSource optionsSource)
{
InitializeWithWindow.Initialize(picker, optionsSource.WindowOptions.Hwnd);
}
HWND hwnd = currentWindowReference.GetWindowHandle();
InitializeWithWindow.Initialize(picker, hwnd);
return picker;
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Factory.Progress;
internal interface IProgressFactory
{
IProgress<T> CreateForMainThread<T>(Action<T> handler);
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Snap.Hutao.Factory.Progress;
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IProgressFactory))]
internal sealed partial class ProgressFactory : IProgressFactory
{
private readonly ITaskContext taskContext;
public IProgress<T> CreateForMainThread<T>(Action<T> handler)
{
return new DispatcherQueueProgress<T>(handler, taskContext.GetSynchronizationContext());
}
}

View File

@@ -0,0 +1,549 @@
// <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("20231103032056_AddUserFingerprint")]
partial class AddUserFingerprint
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.13");
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<string>("Fingerprint")
.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,29 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
/// <inheritdoc />
public partial class AddUserFingerprint : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Fingerprint",
table: "users",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Fingerprint",
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.10");
modelBuilder.HasAnnotation("ProductVersion", "7.0.13");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
@@ -400,7 +400,7 @@ namespace Snap.Hutao.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("ScheduleId")
b.Property<uint>("ScheduleId")
.HasColumnType("INTEGER");
b.Property<string>("SpiralAbyss")
@@ -428,6 +428,9 @@ namespace Snap.Hutao.Migrations
b.Property<string>("CookieToken")
.HasColumnType("TEXT");
b.Property<string>("Fingerprint")
.HasColumnType("TEXT");
b.Property<bool>("IsOversea")
.HasColumnType("INTEGER");

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model;
internal static class CollectionsNameValue
{
public static List<NameValue<T>> ListFromEnum<T>()
where T : struct, Enum
{
return Enum.GetValues<T>().Select(x => new NameValue<T>(x.ToString(), x)).ToList();
}
}

View File

@@ -48,6 +48,16 @@ internal sealed partial class SettingEntry
/// </summary>
public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame";
/// <summary>
/// 实时便笺 WebhookUrl
/// </summary>
public const string DailyNoteWebhookUrl = "DailyNote.WebhookUrl";
/// <summary>
/// 启动游戏 总开关
/// </summary>
public const string LaunchIsLaunchOptionsEnabled = "Launch.IsLaunchOptionsEnabled";
/// <summary>
/// 启动游戏 独占全屏
/// </summary>
@@ -68,11 +78,15 @@ internal sealed partial class SettingEntry
/// </summary>
public const string LaunchScreenWidth = "Launch.ScreenWidth";
public const string LaunchIsScreenWidthEnabled = "Launch.IsScreenWidthEnabled";
/// <summary>
/// 启动游戏 高度
/// </summary>
public const string LaunchScreenHeight = "Launch.ScreenHeight";
public const string LaunchIsScreenHeightEnabled = "Launch.IsScreenHeightEnabled";
/// <summary>
/// 启动游戏 解锁帧率
/// </summary>
@@ -88,6 +102,10 @@ internal sealed partial class SettingEntry
/// </summary>
public const string LaunchMonitor = "Launch.Monitor";
public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled";
public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics";
/// <summary>
/// 启动游戏 多倍启动
/// </summary>

View File

@@ -60,6 +60,11 @@ internal sealed class User : ISelectable, IMappingFrom<User, Cookie, bool>
/// </summary>
public bool IsOversea { get; set; }
/// <summary>
/// 用户指纹 Id
/// </summary>
public string? Fingerprint { get; set; }
/// <summary>
/// 创建一个新的用户
/// </summary>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Model.InterChange.GachaLog;
@@ -10,12 +11,12 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// https://uigf.org/standards/UIGF.html
/// </summary>
[HighQuality]
internal sealed class UIGF
internal sealed class UIGF : IJsonOnSerializing, IJsonOnDeserialized
{
/// <summary>
/// 当前版本
/// </summary>
public const string CurrentVersion = "v2.3";
public const string CurrentVersion = "v2.4";
/// <summary>
/// 信息
@@ -30,11 +31,27 @@ internal sealed class UIGF
[JsonPropertyName("list")]
public List<UIGFItem> List { get; set; } = default!;
/// <summary>
/// 确认当前UIGF对象的版本是否受支持
/// </summary>
/// <param name="version">版本</param>
/// <returns>当前UIAF对象是否受支持</returns>
public void OnSerializing()
{
TimeSpan offset = GetRegionTimeZoneUtcOffset();
foreach (UIGFItem item in List)
{
item.Time = item.Time.ToOffset(offset);
}
}
public void OnDeserialized()
{
// Adjust items timezone
TimeSpan offset = GetRegionTimeZoneUtcOffset();
foreach (UIGFItem item in List)
{
item.Time = UnsafeDateTimeOffset.AdjustOffsetOnly(item.Time, offset);
}
}
public bool IsCurrentVersionSupported(out UIGFVersion version)
{
version = Info.UIGFVersion switch
@@ -42,17 +59,13 @@ internal sealed class UIGF
"v2.1" => UIGFVersion.Major2Minor2OrLower,
"v2.2" => UIGFVersion.Major2Minor2OrLower,
"v2.3" => UIGFVersion.Major2Minor3OrHigher,
"v2.4" => UIGFVersion.Major2Minor3OrHigher,
_ => UIGFVersion.NotSupported,
};
return version != UIGFVersion.NotSupported;
}
/// <summary>
/// 列表物品是否正常
/// </summary>
/// <param name="id">首个出错的Id</param>
/// <returns>是否正常</returns>
public bool IsMajor2Minor2OrLowerListValid([NotNullWhen(false)] out long id)
{
foreach (ref readonly UIGFItem item in CollectionsMarshal.AsSpan(List))
@@ -82,4 +95,14 @@ internal sealed class UIGF
id = 0;
return true;
}
private TimeSpan GetRegionTimeZoneUtcOffset()
{
if (Info.RegionTimeZone is int offsetHours)
{
return new TimeSpan(offsetHours, 0, 0);
}
return PlayerUid.GetRegionTimeZoneUtcOffset(Info.Uid);
}
}

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Core;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Model.InterChange.GachaLog;
@@ -58,6 +59,12 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
[JsonPropertyName("uigf_version")]
public string UIGFVersion { get; set; } = default!;
/// <summary>
/// 时区偏移
/// </summary>
[JsonPropertyName("region_time_zone")]
public int? RegionTimeZone { get; set; } = default!;
public static UIGFInfo From(RuntimeOptions runtimeOptions, MetadataOptions metadataOptions, string uid)
{
return new()
@@ -68,6 +75,7 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
ExportApp = SH.AppName,
ExportAppVersion = runtimeOptions.Version.ToString(),
UIGFVersion = UIGF.CurrentVersion,
RegionTimeZone = PlayerUid.GetRegionTimeZoneUtcOffset(uid).Hours,
};
}
}

View File

@@ -14,12 +14,17 @@ internal enum Arkhe
None,
/// <summary>
///
///
/// </summary>
Ousia,
/// <summary>
///
///
/// </summary>
Pneuma,
/// <summary>
/// 圣俗杂座
/// </summary>
Furina,
}

View File

@@ -59,4 +59,5 @@ internal enum MaterialType
MATERIAL_GCG_EXCHANGE_ITEM = 48,
MATERIAL_QUEST_EVENT_BOOK = 49,
MATERIAL_PROFILE_PICTURE = 50,
MATERIAL_RAINBOW_PRINCE_HAND_BOOK = 51,
}

View File

@@ -19,6 +19,8 @@ internal static class MonsterRelationship
5071U => 507U, // 幻形花鼠 · 水 (强化)
5102U => 510U, // 历经百战的浊水粉碎幻灵
5112U => 511U, // 历经百战的浊水喷吐幻灵
30605U => 30603U, // 历经百战的霜剑律从
30606U => 30604U, // 历经百战的幽风铃兰
60402U => 60401U, // (火)岩龙蜥
60403U => 60401U, // (冰)岩龙蜥
60404U => 60401U, // (雷)岩龙蜥

View File

@@ -6,6 +6,8 @@ namespace Snap.Hutao.Model;
/// <summary>
/// 封装带有名称描述的值
/// 在绑定枚举变量时非常有用
/// https://github.com/microsoft/microsoft-ui-xaml/issues/4266
/// 直接绑定枚举变量会显示 Windows.Foundation.IReference{T}
/// </summary>
/// <typeparam name="T">包含值的类型</typeparam>
[HighQuality]

View File

@@ -12,7 +12,7 @@
<Identity
Name="60568DGPStudio.SnapHutao"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.7.14.0" />
Version="1.7.17.0" />
<Properties>
<DisplayName>Snap Hutao</DisplayName>

View File

@@ -539,9 +539,15 @@
<data name="ServerPassportVerifyFailed" xml:space="preserve">
<value>Verification failed</value>
</data>
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
<value>The verification request failed, it is not the currently logged in account</value>
</data>
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
<value>The verification code has been sent to your e-mail.</value>
</data>
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
<value>The verification request failed, the current email address has been registered</value>
</data>
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
<value>Validation request is too frequent. Please try again in 1 minute.</value>
</data>
@@ -1796,6 +1802,9 @@
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
<value>Input</value>
</data>
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
<value>Recover Wish Record from Snap Hutao Cloud</value>
</data>
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
<value>Refresh</value>
</data>
@@ -1958,6 +1967,12 @@
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
<value>Advanced Features</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
<value>Resolution Ratio Shortcut</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>Screen Resolution</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>Create window as popup, without frame</value>
</data>
@@ -2006,6 +2021,9 @@
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>File</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>InterProcess</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>Run the software on the selected display</value>
</data>
@@ -2021,6 +2039,12 @@
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
<value>Game Options</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>Try to start the game after the game is started and use Starward for game duration statistics</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>Hours Played</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>Progress</value>
</data>
@@ -2276,6 +2300,15 @@
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
<value>Enable Advanced Features</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
<value>Change Auto Click Shortcut</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
<value>Auto Click</value>
</data>
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>Shortcut Keys</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>Official Website</value>
</data>
@@ -2486,6 +2519,9 @@
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
<value>Upload Data</value>
</data>
<data name="ViewTitleAutoClicking" xml:space="preserve">
<value>Auto Click</value>
</data>
<data name="ViewToolHeader" xml:space="preserve">
<value>Tools</value>
</data>
@@ -2499,7 +2535,7 @@
<value>Current user</value>
</data>
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
<value>My Characters</value>
<value>Official Tools</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>Web Login</value>
@@ -2714,6 +2750,12 @@
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
<value>Weapon Event Wish</value>
</data>
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>Copy Link Successful</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>Invalid UID</value>
</data>
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
<value>Verification failed. Please verify manually or check MiHoYo BBS - My Characters page</value>
</data>

View File

@@ -539,9 +539,15 @@
<data name="ServerPassportVerifyFailed" xml:space="preserve">
<value>認証に失敗しました。</value>
</data>
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
<value>認証リクエストに失敗しました。現在ログインしているアカウントではありません。</value>
</data>
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
<value>入力されたメールアドレスへ確認コードが送信されました</value>
</data>
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
<value>認証リクエストに失敗しました。このメールアドレスは既に登録されています。</value>
</data>
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
<value>認証リクエストが多すぎます。一分後にやり直してください。</value>
</data>
@@ -1796,6 +1802,9 @@
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
<value>入力</value>
</data>
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
<value>胡桃クラウドから祈願履歴を復元する</value>
</data>
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
<value>情報更新</value>
</data>
@@ -1958,6 +1967,12 @@
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
<value>上級者向け設定</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
<value>指定した解像度に素早く切り替えます</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>解像度</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>枠の無いポップアップウィンドウとして作成します。</value>
</data>
@@ -2006,6 +2021,9 @@
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>ファイル</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>インタープロセス</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>指定したディスプレイで実行</value>
</data>
@@ -2021,6 +2039,12 @@
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
<value>ゲームオプション</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>ゲームの開始後にStarward ランチャーを起動し、プレイ時間の統計を確認してみてください。</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>プレイ時間</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>プロセス</value>
</data>
@@ -2276,6 +2300,15 @@
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
<value>上級者向け設定を有効にする</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
<value>オートクリック機能のショートカットキーを変更します</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
<value>オートクリック</value>
</data>
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>ショートカットキー</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>公式サイト</value>
</data>
@@ -2486,6 +2519,9 @@
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
<value>データをアップロード</value>
</data>
<data name="ViewTitleAutoClicking" xml:space="preserve">
<value>オートクリック</value>
</data>
<data name="ViewToolHeader" xml:space="preserve">
<value>ツールボックス</value>
</data>
@@ -2499,7 +2535,7 @@
<value>現在のユーザー</value>
</data>
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
<value>マイ キャラクター</value>
<value>旅行ツール</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>ウェブ上でログイン</value>
@@ -2622,7 +2658,7 @@
<value>デイリー依頼の報酬は受け取り済みです</value>
</data>
<data name="WebDailyNoteHomeCoinRecoveryFormat" xml:space="preserve">
<value>上限到達まで{0} {1:HH:mm}</value>
<value>{0} {1:HH:mm} に上限到達</value>
</data>
<data name="WebDailyNoteHomeLocked" xml:space="preserve">
<value>塵歌壺が未解放です</value>
@@ -2714,6 +2750,12 @@
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
<value>イベント祈願・武器</value>
</data>
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>ダウンロードリンクのコピーに成功しました</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>无效的 UID</value>
</data>
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
<value>認証に失敗しました。 手動で認証するか、MiHoYo BBS - 戦績 を確認してください。</value>
</data>

View File

@@ -539,9 +539,15 @@
<data name="ServerPassportVerifyFailed" xml:space="preserve">
<value>验证失败</value>
</data>
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
<value>验证请求失败,不是当前登录的账号</value>
</data>
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
<value>验证码已发送至邮箱</value>
</data>
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
<value>验证请求失败,当前邮箱已被注册</value>
</data>
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
<value>验证请求过快,请 1 分钟后再试</value>
</data>
@@ -1796,6 +1802,9 @@
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
<value>입력</value>
</data>
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
<value>从胡桃云恢复祈愿记录</value>
</data>
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
<value>동기화</value>
</data>
@@ -1958,6 +1967,12 @@
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
<value>고급</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
<value>快速切换到指定的分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>테두리 없는 창모드</value>
</data>
@@ -2006,6 +2021,9 @@
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>文件</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程间</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>지정한 모니터에서 실행</value>
</data>
@@ -2021,6 +2039,12 @@
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
<value>게임 설정</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>时长统计</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>进程</value>
</data>
@@ -2276,6 +2300,15 @@
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
<value>고급 기능 활성화</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
<value>更改自动连点功能的快捷键</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
<value>自动连点</value>
</data>
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>快捷键</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>공식 홈페이지로 이동</value>
</data>
@@ -2486,6 +2519,9 @@
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
<value>데이터 업로드</value>
</data>
<data name="ViewTitleAutoClicking" xml:space="preserve">
<value>自动连点</value>
</data>
<data name="ViewToolHeader" xml:space="preserve">
<value>도구</value>
</data>
@@ -2499,7 +2535,7 @@
<value>当前用户</value>
</data>
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
<value>我的角色</value>
<value>旅行工具</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>웹 로그인</value>
@@ -2714,6 +2750,12 @@
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
<value>무기 이벤트 기원</value>
</data>
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>下载链接复制成功</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>无效的 UID</value>
</data>
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
<value>验证失败,请手动验证或前往「米游社-我的角色」页面查看</value>
</data>

View File

@@ -539,9 +539,15 @@
<data name="ServerPassportVerifyFailed" xml:space="preserve">
<value>验证失败</value>
</data>
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
<value>验证请求失败,不是当前登录的账号</value>
</data>
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
<value>验证码已发送至邮箱</value>
</data>
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
<value>验证请求失败,当前邮箱已被注册</value>
</data>
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
<value>验证请求过快,请 1 分钟后再试</value>
</data>
@@ -1796,6 +1802,9 @@
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
<value>输入</value>
</data>
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
<value>从胡桃云恢复祈愿记录</value>
</data>
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
<value>刷新</value>
</data>
@@ -1958,6 +1967,12 @@
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
<value>高级功能</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
<value>快速切换到指定的分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>将窗口创建为弹出窗口,不带框架</value>
</data>
@@ -2006,6 +2021,9 @@
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>文件</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程间</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>在指定的显示器上运行</value>
</data>
@@ -2021,6 +2039,12 @@
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
<value>游戏选项</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>时长统计</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>进程</value>
</data>
@@ -2276,6 +2300,15 @@
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
<value>启动高级功能</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
<value>更改自动连点功能的快捷键</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
<value>自动连点</value>
</data>
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>快捷键</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>前往官网</value>
</data>
@@ -2486,6 +2519,9 @@
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
<value>上传数据</value>
</data>
<data name="ViewTitleAutoClicking" xml:space="preserve">
<value>自动连点</value>
</data>
<data name="ViewToolHeader" xml:space="preserve">
<value>工具</value>
</data>
@@ -2499,7 +2535,7 @@
<value>当前用户</value>
</data>
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
<value>我的角色</value>
<value>旅行工具</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>网页登录</value>
@@ -2714,6 +2750,12 @@
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
<value>武器活动祈愿</value>
</data>
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>下载链接复制成功</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>无效的 UID</value>
</data>
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
<value>验证失败,请手动验证或前往「米游社-我的角色」页面查看</value>
</data>

View File

@@ -539,9 +539,15 @@
<data name="ServerPassportVerifyFailed" xml:space="preserve">
<value>验证失败</value>
</data>
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
<value>验证请求失败,不是当前登录的账号</value>
</data>
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
<value>验证码已发送至邮箱</value>
</data>
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
<value>验证请求失败,当前邮箱已被注册</value>
</data>
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
<value>验证请求过快,请 1 分钟后再试</value>
</data>
@@ -1689,7 +1695,7 @@
<value>新增</value>
</data>
<data name="ViewPageDailyNoteAttendanceStatusInfo" xml:space="preserve">
<value>历练点获取详情</value>
<value>歷練點獲取詳情</value>
</data>
<data name="ViewPageDailyNoteConfigWebhookDescription" xml:space="preserve">
<value>在实时便笺刷新后推送到指定的 Webhook</value>
@@ -1796,6 +1802,9 @@
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
<value>輸入</value>
</data>
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
<value>從胡桃云恢復祈願紀錄</value>
</data>
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
<value>重新整理</value>
</data>
@@ -1958,6 +1967,12 @@
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
<value>進階功能</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
<value>快速切换到指定的分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>將窗口創建為彈出窗口,不帶邊框</value>
</data>
@@ -2006,6 +2021,9 @@
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>文件</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程间</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>在指定的屏幕上運行</value>
</data>
@@ -2021,6 +2039,12 @@
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
<value>遊戲選項</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>时长统计</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>进程</value>
</data>
@@ -2276,6 +2300,15 @@
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
<value>啟動高級功能</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
<value>更改自动连点功能的快捷键</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
<value>自动连点</value>
</data>
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>快捷键</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>前往官網</value>
</data>
@@ -2486,6 +2519,9 @@
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
<value>上傳資料</value>
</data>
<data name="ViewTitleAutoClicking" xml:space="preserve">
<value>自动连点</value>
</data>
<data name="ViewToolHeader" xml:space="preserve">
<value>工具</value>
</data>
@@ -2499,7 +2535,7 @@
<value>當前用戶</value>
</data>
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
<value>我的角色</value>
<value>旅行工具</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>網頁登陸</value>
@@ -2714,6 +2750,12 @@
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
<value>武器活動祈願</value>
</data>
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>下载链接复制成功</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>无效的 UID</value>
</data>
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
<value>验证失败,请手动验证或前往「米游社-我的角色」页面查看</value>
</data>

View File

@@ -19,12 +19,7 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions
{
private readonly List<NameValue<BackdropType>> supportedBackdropTypesInner = new()
{
new("Acrylic", BackdropType.Acrylic),
new("Mica", BackdropType.Mica),
new("MicaAlt", BackdropType.MicaAlt),
};
private readonly List<NameValue<BackdropType>> supportedBackdropTypesInner = CollectionsNameValue.ListFromEnum<BackdropType>();
private readonly List<NameValue<string>> supportedCulturesInner = new()
{
@@ -106,6 +101,8 @@ internal sealed partial class AppOptions : DbStoreOptions
/// <summary>
/// 是否启用高级功能
/// DO NOT MOVE TO OTHER CLASS
/// We are binding this property in SettingPage
/// </summary>
public bool IsAdvancedLaunchOptionsEnabled
{

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Service;
internal static class AppOptionsExtension
{
public static bool TryGetGameFolderAndFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
{
string gamePath = appOptions.GamePath;
gameFolder = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameFolder))
{
gameFileName = default;
return false;
}
gameFileName = Path.GetFileName(gamePath);
if (string.IsNullOrEmpty(gameFileName))
{
return false;
}
return true;
}
public static bool TryGetGameFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFileName)
{
string gamePath = appOptions.GamePath;
gameFileName = Path.GetFileName(gamePath);
if (string.IsNullOrEmpty(gameFileName))
{
return false;
}
return true;
}
}

View File

@@ -24,7 +24,7 @@ internal sealed partial class DailyNoteNotificationOperation
private const string ToastAttributionUnknown = "Unknown";
private readonly ITaskContext taskContext;
private readonly IGameService gameService;
private readonly IGameServiceFacade gameService;
private readonly BindingClient bindingClient;
private readonly DailyNoteOptions options;

View File

@@ -126,7 +126,7 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
public string? WebhookUrl
{
get => GetOption(ref webhookUrl, SettingEntry.DailyNoteSilentWhenPlayingGame);
set => SetOption(ref webhookUrl, SettingEntry.DailyNoteSilentWhenPlayingGame, value);
get => GetOption(ref webhookUrl, SettingEntry.DailyNoteWebhookUrl);
set => SetOption(ref webhookUrl, SettingEntry.DailyNoteWebhookUrl, value);
}
}

View File

@@ -120,6 +120,7 @@ internal sealed class TypedWishSummaryBuilder
{
// base
Name = context.Name,
TypeName = $"{context.DistributionType:D}",
From = fromTimeTracker,
To = toTimeTracker,
TotalCount = totalCountTracker,

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Request.QueryString;

View File

@@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
[Injection(InjectAs.Transient)]
internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider
{
private readonly IGameService gameService;
private readonly IGameServiceFacade gameService;
private readonly MetadataOptions metadataOptions;
/// <summary>
@@ -104,7 +104,7 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
{
ReadOnlySpan<byte> span = stream.ToArray();
ReadOnlySpan<byte> match = isOversea
? "https://webstatic-sea.hoyoverse.com/genshin/event/e20190909gacha-v2/index.html"u8
? "https://gs.hoyoverse.com/genshin/event/e20190909gacha-v2/index.html"u8
: "https://webstatic.mihoyo.com/hk4e/event/e20190909gacha-v2/index.html"u8;
int index = span.LastIndexOf(match);

View File

@@ -0,0 +1,133 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.View.Dialog;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game.Account;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGameAccountService))]
internal sealed partial class GameAccountService : IGameAccountService
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IServiceProvider serviceProvider;
private readonly IGameDbService gameDbService;
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
private ObservableCollection<GameAccount>? gameAccounts;
public ObservableCollection<GameAccount> GameAccountCollection
{
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
}
public async ValueTask<GameAccount?> DetectGameAccountAsync()
{
ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
{
GameAccount? account = null;
try
{
account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
if (account is null)
{
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
account = GameAccount.From(name, registrySdk);
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false);
// sync cache
await taskContext.SwitchToMainThreadAsync();
gameAccounts.Add(account);
}
}
return account;
}
return default;
}
public GameAccount? DetectCurrentGameAccount()
{
ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
{
try
{
return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
}
return null;
}
public bool SetGameAccount(GameAccount account)
{
if (string.IsNullOrEmpty(appOptions.PowerShellPath))
{
ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!);
}
return RegistryInterop.Set(account, appOptions.PowerShellPath);
}
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
{
gameAccount.UpdateAttachUid(uid);
gameDbService.UpdateGameAccount(gameAccount);
}
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
{
await taskContext.SwitchToMainThreadAsync();
LaunchGameAccountNameDialog dialog = serviceProvider.CreateInstance<LaunchGameAccountNameDialog>();
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true);
if (isOk)
{
gameAccount.UpdateName(name);
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.UpdateGameAccountAsync(gameAccount).ConfigureAwait(false);
}
}
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
{
await taskContext.SwitchToMainThreadAsync();
ArgumentNullException.ThrowIfNull(gameAccounts);
gameAccounts.Remove(gameAccount);
await taskContext.SwitchToBackgroundAsync();
await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game.Account;
internal interface IGameAccountService
{
ObservableCollection<GameAccount> GameAccountCollection { get; }
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
GameAccount? DetectCurrentGameAccount();
ValueTask<GameAccount?> DetectGameAccountAsync();
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
bool SetGameAccount(GameAccount account);
}

View File

@@ -9,7 +9,7 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace Snap.Hutao.Service.Game;
namespace Snap.Hutao.Service.Game.Account;
/// <summary>
/// 注册表操作
@@ -55,7 +55,7 @@ internal static class RegistryInterop
try
{
Process.Start(startInfo)?.WaitForExit();
System.Diagnostics.Process.Start(startInfo)?.WaitForExit();
}
catch (Win32Exception ex)
{

View File

@@ -3,7 +3,7 @@
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Service.Game;
namespace Snap.Hutao.Service.Game.Configuration;
/// <summary>
/// 多通道

View File

@@ -0,0 +1,95 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO.Ini;
using Snap.Hutao.Service.Game.Scheme;
using System.IO;
using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Configuration;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGameChannelOptionsService))]
internal sealed partial class GameChannelOptionsService : IGameChannelOptionsService
{
private readonly AppOptions appOptions;
public ChannelOptions GetChannelOptions()
{
string gamePath = appOptions.GamePath;
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase);
if (!File.Exists(configPath))
{
return ChannelOptions.FileNotFound(isOversea, configPath);
}
using (FileStream stream = File.OpenRead(configPath))
{
List<IniParameter> parameters = IniSerializer.Deserialize(stream).OfType<IniParameter>().ToList();
string? channel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.ChannelName)?.Value;
string? subChannel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.SubChannelName)?.Value;
return new(channel, subChannel, isOversea);
}
}
public bool SetChannelOptions(LaunchScheme scheme)
{
string gamePath = appOptions.GamePath;
string? directory = Path.GetDirectoryName(gamePath);
ArgumentException.ThrowIfNullOrEmpty(directory);
string configPath = Path.Combine(directory, ConfigFileName);
List<IniElement> elements = default!;
try
{
using (FileStream readStream = File.OpenRead(configPath))
{
elements = IniSerializer.Deserialize(readStream).ToList();
}
}
catch (FileNotFoundException ex)
{
ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex);
}
catch (DirectoryNotFoundException ex)
{
ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex);
}
catch (UnauthorizedAccessException ex)
{
ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelUnauthorizedAccess, ex);
}
bool changed = false;
foreach (IniElement element in elements)
{
if (element is IniParameter parameter)
{
if (parameter.Key == "channel")
{
changed = parameter.Set(scheme.Channel.ToString("D")) || changed;
}
if (parameter.Key == "sub_channel")
{
changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed;
}
}
}
if (changed)
{
using (FileStream writeStream = File.Create(configPath))
{
IniSerializer.Serialize(writeStream, elements);
}
}
return changed;
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Scheme;
namespace Snap.Hutao.Service.Game.Configuration;
internal interface IGameChannelOptionsService
{
ChannelOptions GetChannelOptions();
bool SetChannelOptions(LaunchScheme scheme);
}

View File

@@ -4,7 +4,7 @@
using Snap.Hutao.Model.Intrinsic;
using System.Collections.Immutable;
namespace Snap.Hutao.Service.Game;
namespace Snap.Hutao.Service.Game.Configuration;
internal static class IgnoredInvalidChannelOptions
{

View File

@@ -0,0 +1,60 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Locator;
namespace Snap.Hutao.Service.Game;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGamePathService))]
internal sealed partial class GamePathService : IGamePathService
{
private readonly IServiceProvider serviceProvider;
private readonly AppOptions appOptions;
public async ValueTask<ValueResult<bool, string>> SilentGetGamePathAsync()
{
// Cannot find in setting
if (string.IsNullOrEmpty(appOptions.GamePath))
{
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
bool isOk;
string path;
// Try locate by unity log
(isOk, path) = await locatorFactory
.Create(GameLocationSource.UnityLog)
.LocateGamePathAsync()
.ConfigureAwait(false);
if (!isOk)
{
// Try locate by registry
(isOk, path) = await locatorFactory
.Create(GameLocationSource.Registry)
.LocateGamePathAsync()
.ConfigureAwait(false);
}
if (isOk)
{
// Save result.
appOptions.GamePath = path;
}
else
{
return new(false, SH.ServiceGamePathLocateFailed);
}
}
if (!string.IsNullOrEmpty(appOptions.GamePath))
{
return new(true, appOptions.GamePath);
}
else
{
return new(false, default!);
}
}
}

View File

@@ -1,395 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO.Ini;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using Snap.Hutao.Web.Response;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 游戏服务
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGameService))]
internal sealed partial class GameService : IGameService
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly PackageConverter packageConverter;
private readonly IServiceProvider serviceProvider;
private readonly IGameDbService gameDbService;
private readonly LaunchOptions launchOptions;
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
private volatile int runningGamesCounter;
private ObservableCollection<GameAccount>? gameAccounts;
/// <inheritdoc/>
public ObservableCollection<GameAccount> GameAccountCollection
{
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
}
/// <inheritdoc/>
public async ValueTask<ValueResult<bool, string>> GetGamePathAsync()
{
// Cannot find in setting
if (string.IsNullOrEmpty(appOptions.GamePath))
{
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
// Try locate by unity log
ValueResult<bool, string> result = await locatorFactory
.Create(GameLocationSource.UnityLog)
.LocateGamePathAsync()
.ConfigureAwait(false);
if (!result.IsOk)
{
// Try locate by registry
result = await locatorFactory
.Create(GameLocationSource.Registry)
.LocateGamePathAsync()
.ConfigureAwait(false);
}
if (result.IsOk)
{
// Save result.
appOptions.GamePath = result.Value;
}
else
{
return new(false, SH.ServiceGamePathLocateFailed);
}
}
if (!string.IsNullOrEmpty(appOptions.GamePath))
{
return new(true, appOptions.GamePath);
}
else
{
return new(false, default!);
}
}
/// <inheritdoc/>
public ChannelOptions GetChannelOptions()
{
string gamePath = appOptions.GamePath;
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase);
if (!File.Exists(configPath))
{
return ChannelOptions.FileNotFound(isOversea, configPath);
}
using (FileStream stream = File.OpenRead(configPath))
{
List<IniParameter> parameters = IniSerializer.Deserialize(stream).OfType<IniParameter>().ToList();
string? channel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.ChannelName)?.Value;
string? subChannel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.SubChannelName)?.Value;
return new(channel, subChannel, isOversea);
}
}
/// <inheritdoc/>
public bool SetChannelOptions(LaunchScheme scheme)
{
string gamePath = appOptions.GamePath;
string? directory = Path.GetDirectoryName(gamePath);
ArgumentException.ThrowIfNullOrEmpty(directory);
string configPath = Path.Combine(directory, ConfigFileName);
List<IniElement> elements = default!;
try
{
using (FileStream readStream = File.OpenRead(configPath))
{
elements = IniSerializer.Deserialize(readStream).ToList();
}
}
catch (FileNotFoundException ex)
{
ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex);
}
catch (DirectoryNotFoundException ex)
{
ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelConfigFileNotFound.Format(configPath), ex);
}
catch (UnauthorizedAccessException ex)
{
ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelUnauthorizedAccess, ex);
}
bool changed = false;
foreach (IniElement element in elements)
{
if (element is IniParameter parameter)
{
if (parameter.Key == "channel")
{
changed = parameter.Set(scheme.Channel.ToString("D")) || changed;
}
if (parameter.Key == "sub_channel")
{
changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed;
}
}
}
if (changed)
{
using (FileStream writeStream = File.Create(configPath))
{
IniSerializer.Serialize(writeStream, elements);
}
}
return changed;
}
/// <inheritdoc/>
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
string gamePath = appOptions.GamePath;
string? gameFolder = Path.GetDirectoryName(gamePath);
ArgumentException.ThrowIfNullOrEmpty(gameFolder);
string gameFileName = Path.GetFileName(gamePath);
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
Response<GameResource> response = await serviceProvider
.GetRequiredService<ResourceClient>()
.GetResourceAsync(launchScheme)
.ConfigureAwait(false);
if (response.IsOk())
{
GameResource resource = response.Data;
if (!launchScheme.ExecutableMatches(gameFileName))
{
bool replaced = await packageConverter
.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress)
.ConfigureAwait(false);
if (replaced)
{
// We need to change the gamePath if we switched.
string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName;
await taskContext.SwitchToMainThreadAsync();
appOptions.GamePath = Path.Combine(gameFolder, exeName);
}
else
{
// We can't start the game
// when we failed to convert game
return false;
}
}
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
return true;
}
return false;
}
/// <inheritdoc/>
public bool IsGameRunning()
{
if (runningGamesCounter == 0)
{
return false;
}
return Process.GetProcessesByName(YuanShenProcessName).Any()
|| Process.GetProcessesByName(GenshinImpactProcessName).Any();
}
/// <inheritdoc/>
public async ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
{
if (IsGameRunning())
{
return;
}
string gamePath = appOptions.GamePath;
ArgumentException.ThrowIfNullOrEmpty(gamePath);
progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
using (Process game = ProcessInterop.InitializeGameProcess(launchOptions, gamePath))
{
try
{
Interlocked.Increment(ref runningGamesCounter);
game.Start();
progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted));
if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
{
progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
try
{
await ProcessInterop.UnlockFpsAsync(serviceProvider, game, progress).ConfigureAwait(false);
}
catch (InvalidOperationException)
{
// The Unlocker can't unlock the process
game.Kill();
throw;
}
finally
{
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
}
}
else
{
progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit));
await game.WaitForExitAsync().ConfigureAwait(false);
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
}
}
finally
{
Interlocked.Decrement(ref runningGamesCounter);
}
}
}
/// <inheritdoc/>
public async ValueTask<GameAccount?> DetectGameAccountAsync()
{
ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
{
GameAccount? account = null;
try
{
account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
if (account is null)
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
account = GameAccount.From(name, registrySdk);
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false);
// sync cache
await taskContext.SwitchToMainThreadAsync();
gameAccounts.Add(account);
}
}
return account;
}
return default;
}
/// <inheritdoc/>
public GameAccount? DetectCurrentGameAccount()
{
ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
{
try
{
return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
}
return null;
}
/// <inheritdoc/>
public bool SetGameAccount(GameAccount account)
{
if (string.IsNullOrEmpty(appOptions.PowerShellPath))
{
ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!);
}
return RegistryInterop.Set(account, appOptions.PowerShellPath);
}
/// <inheritdoc/>
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
{
gameAccount.UpdateAttachUid(uid);
gameDbService.UpdateGameAccount(gameAccount);
}
/// <inheritdoc/>
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
{
await taskContext.SwitchToMainThreadAsync();
LaunchGameAccountNameDialog dialog = serviceProvider.CreateInstance<LaunchGameAccountNameDialog>();
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true);
if (isOk)
{
gameAccount.UpdateName(name);
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.UpdateGameAccountAsync(gameAccount).ConfigureAwait(false);
}
}
/// <inheritdoc/>
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
{
await taskContext.SwitchToMainThreadAsync();
ArgumentNullException.ThrowIfNull(gameAccounts);
gameAccounts.Remove(gameAccount);
await taskContext.SwitchToBackgroundAsync();
await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,105 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.Process;
using Snap.Hutao.Service.Game.Scheme;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 游戏服务
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGameServiceFacade))]
internal sealed partial class GameServiceFacade : IGameServiceFacade
{
private readonly IGameChannelOptionsService gameChannelOptionsService;
private readonly IGameAccountService gameAccountService;
private readonly IGameProcessService gameProcessService;
private readonly IGamePackageService gamePackageService;
private readonly IGamePathService gamePathService;
/// <inheritdoc/>
public ObservableCollection<GameAccount> GameAccountCollection
{
get => gameAccountService.GameAccountCollection;
}
/// <inheritdoc/>
public ValueTask<ValueResult<bool, string>> GetGamePathAsync()
{
return gamePathService.SilentGetGamePathAsync();
}
/// <inheritdoc/>
public ChannelOptions GetChannelOptions()
{
return gameChannelOptionsService.GetChannelOptions();
}
/// <inheritdoc/>
public bool SetChannelOptions(LaunchScheme scheme)
{
return gameChannelOptionsService.SetChannelOptions(scheme);
}
/// <inheritdoc/>
public ValueTask<GameAccount?> DetectGameAccountAsync()
{
return gameAccountService.DetectGameAccountAsync();
}
/// <inheritdoc/>
public GameAccount? DetectCurrentGameAccount()
{
return gameAccountService.DetectCurrentGameAccount();
}
/// <inheritdoc/>
public bool SetGameAccount(GameAccount account)
{
return gameAccountService.SetGameAccount(account);
}
/// <inheritdoc/>
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
{
gameAccountService.AttachGameAccountToUid(gameAccount, uid);
}
/// <inheritdoc/>
public ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
{
return gameAccountService.ModifyGameAccountAsync(gameAccount);
}
/// <inheritdoc/>
public ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
{
return gameAccountService.RemoveGameAccountAsync(gameAccount);
}
/// <inheritdoc/>
public bool IsGameRunning()
{
return gameProcessService.IsGameRunning();
}
/// <inheritdoc/>
public ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
{
return gameProcessService.LaunchAsync(progress);
}
/// <inheritdoc/>
public ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
return gamePackageService.EnsureGameResourceAsync(launchScheme, progress);
}
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game;
internal interface IGamePathService
{
ValueTask<ValueResult<bool, string>> SilentGetGamePathAsync();
}

View File

@@ -2,7 +2,10 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.Scheme;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game;
@@ -11,7 +14,7 @@ namespace Snap.Hutao.Service.Game;
/// 游戏服务
/// </summary>
[HighQuality]
internal interface IGameService
internal interface IGameServiceFacade
{
/// <summary>
/// 游戏内账号集合

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.Windowing;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
@@ -23,14 +24,20 @@ internal sealed class LaunchOptions : DbStoreOptions
private readonly int primaryScreenHeight;
private readonly int primaryScreenFps;
private bool? isEnabled;
private bool? isFullScreen;
private bool? isBorderless;
private bool? isExclusive;
private int? screenWidth;
private bool? isScreenWidthEnabled;
private int? screenHeight;
private bool? isScreenHeightEnabled;
private bool? unlockFps;
private int? targetFps;
private NameValue<int>? monitor;
private bool? isMonitorEnabled;
private AspectRatio? selectedAspectRatio;
private bool? useStarwardPlayTimeStatistics;
/// <summary>
/// 构造一个新的启动游戏选项
@@ -47,6 +54,15 @@ internal sealed class LaunchOptions : DbStoreOptions
InitializeScreenFps(out primaryScreenFps);
}
/// <summary>
/// 是否启用启动参数
/// </summary>
public bool IsEnabled
{
get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true);
set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value);
}
/// <summary>
/// 是否全屏
/// </summary>
@@ -83,6 +99,12 @@ internal sealed class LaunchOptions : DbStoreOptions
set => SetOption(ref screenWidth, SettingEntry.LaunchScreenWidth, value);
}
public bool IsScreenWidthEnabled
{
get => GetOption(ref isScreenWidthEnabled, SettingEntry.LaunchIsScreenWidthEnabled, true);
set => SetOption(ref isScreenWidthEnabled, SettingEntry.LaunchIsScreenWidthEnabled, value);
}
/// <summary>
/// 屏幕高度
/// </summary>
@@ -92,6 +114,12 @@ internal sealed class LaunchOptions : DbStoreOptions
set => SetOption(ref screenHeight, SettingEntry.LaunchScreenHeight, value);
}
public bool IsScreenHeightEnabled
{
get => GetOption(ref isScreenHeightEnabled, SettingEntry.LaunchIsScreenHeightEnabled, true);
set => SetOption(ref isScreenHeightEnabled, SettingEntry.LaunchIsScreenHeightEnabled, value);
}
/// <summary>
/// 是否全屏
/// </summary>
@@ -131,6 +159,37 @@ internal sealed class LaunchOptions : DbStoreOptions
}
}
public bool IsMonitorEnabled
{
get => GetOption(ref isMonitorEnabled, SettingEntry.LaunchIsMonitorEnabled, true);
set => SetOption(ref isMonitorEnabled, SettingEntry.LaunchIsMonitorEnabled, value);
}
public List<AspectRatio> AspectRatios { get; } = new()
{
new(2560, 1440),
new(1920, 1080),
};
public AspectRatio? SelectedAspectRatio
{
get => selectedAspectRatio;
set
{
if (SetProperty(ref selectedAspectRatio, value) && value is AspectRatio aspectRatio)
{
ScreenWidth = (int)aspectRatio.Width;
ScreenHeight = (int)aspectRatio.Height;
}
}
}
public bool UseStarwardPlayTimeStatistics
{
get => GetOption(ref useStarwardPlayTimeStatistics, SettingEntry.LaunchUseStarwardPlayTimeStatistics, false);
set => SetOption(ref useStarwardPlayTimeStatistics, SettingEntry.LaunchUseStarwardPlayTimeStatistics, value);
}
private static void InitializeMonitors(List<NameValue<int>> monitors)
{
// This list can't use foreach

View File

@@ -1,160 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 方案列表部分
/// </summary>
internal sealed partial class LaunchScheme
{
private const int SdkStaticLauncherChineseId = 18;
private const int SdkStaticLauncherBilibiliId = 17;
private const int SdkStaticLauncherGlobalId = 10;
private const string SdkStaticLauncherChineseKey = "eYd89JmJ";
private const string SdkStaticLauncherBilibiliKey = "KAtdSsoQ";
private const string SdkStaticLauncherGlobalKey = "gcStgarh";
private static readonly LaunchScheme ServerChineseChannelDefaultSubChannelDefaultCompatOnly = new()
{
LauncherId = SdkStaticLauncherChineseId,
Key = SdkStaticLauncherChineseKey,
Channel = ChannelType.Default,
SubChannel = SubChannelType.Default,
IsOversea = false,
IsNotCompatOnly = false,
};
private static readonly LaunchScheme ServerChineseChannelOfficialSubChannelDefault = new()
{
LauncherId = SdkStaticLauncherChineseId,
Key = SdkStaticLauncherChineseKey,
Channel = ChannelType.Official,
SubChannel = SubChannelType.Default,
IsOversea = false,
};
private static readonly LaunchScheme ServerChineseChannelOfficialSubChannelOfficial = new()
{
LauncherId = SdkStaticLauncherChineseId,
Key = SdkStaticLauncherChineseKey,
Channel = ChannelType.Official,
SubChannel = SubChannelType.Official,
IsOversea = false,
};
private static readonly LaunchScheme ServerChineseChannelOfficialSubChannelNoTapTap = new()
{
LauncherId = SdkStaticLauncherChineseId,
Key = SdkStaticLauncherChineseKey,
Channel = ChannelType.Official,
SubChannel = SubChannelType.NoTapTap,
IsOversea = false,
};
private static readonly LaunchScheme ServerChineseChannelOfficialSubChannelEpicCompatOnly = new()
{
LauncherId = SdkStaticLauncherChineseId,
Key = SdkStaticLauncherChineseKey,
Channel = ChannelType.Official,
SubChannel = SubChannelType.Epic,
IsOversea = false,
IsNotCompatOnly = false,
};
private static readonly LaunchScheme ServerChineseChannelBilibiliSubChannelDefault = new()
{
LauncherId = SdkStaticLauncherBilibiliId,
Key = SdkStaticLauncherBilibiliKey,
Channel = ChannelType.Bili,
SubChannel = SubChannelType.Default,
IsOversea = false,
};
private static readonly LaunchScheme ServerChineseChannelBilibiliSubChannelOfficialCompatOnly = new()
{
LauncherId = SdkStaticLauncherBilibiliId,
Key = SdkStaticLauncherBilibiliKey,
Channel = ChannelType.Bili,
SubChannel = SubChannelType.Official,
IsOversea = false,
IsNotCompatOnly = false,
};
private static readonly LaunchScheme ServerGlobalChannelDefaultSubChannelDefaultCompatOnly = new()
{
LauncherId = SdkStaticLauncherGlobalId,
Key = SdkStaticLauncherGlobalKey,
Channel = ChannelType.Default,
SubChannel = SubChannelType.Default,
IsOversea = true,
IsNotCompatOnly = false,
};
private static readonly LaunchScheme ServerGlobalChannelOfficialSubChannelDefault = new()
{
LauncherId = SdkStaticLauncherGlobalId,
Key = SdkStaticLauncherGlobalKey,
Channel = ChannelType.Official,
SubChannel = SubChannelType.Default,
IsOversea = true,
};
private static readonly LaunchScheme ServerGlobalChannelOfficialSubChannelOfficial = new()
{
LauncherId = SdkStaticLauncherGlobalId,
Key = SdkStaticLauncherGlobalKey,
Channel = ChannelType.Official,
SubChannel = SubChannelType.Official,
IsOversea = true,
};
private static readonly LaunchScheme ServerGlobalChannelOfficialSubChannelEpic = new()
{
LauncherId = SdkStaticLauncherGlobalId,
Key = SdkStaticLauncherGlobalKey,
Channel = ChannelType.Official,
SubChannel = SubChannelType.Epic,
IsOversea = true,
};
private static readonly LaunchScheme ServerGlobalChannelOfficialSubChannelGoogle = new()
{
LauncherId = SdkStaticLauncherGlobalId,
Key = SdkStaticLauncherGlobalKey,
Channel = ChannelType.Official,
SubChannel = SubChannelType.Google,
IsOversea = true,
};
/// <summary>
/// 获取已知的启动方案
/// </summary>
/// <returns>已知的启动方案</returns>
public static List<LaunchScheme> GetKnownSchemes()
{
return new List<LaunchScheme>()
{
// 官服
ServerChineseChannelDefaultSubChannelDefaultCompatOnly,
ServerChineseChannelOfficialSubChannelDefault,
ServerChineseChannelOfficialSubChannelOfficial,
ServerChineseChannelOfficialSubChannelNoTapTap,
ServerChineseChannelOfficialSubChannelEpicCompatOnly,
// 渠道服
ServerChineseChannelBilibiliSubChannelDefault,
ServerChineseChannelBilibiliSubChannelOfficialCompatOnly,
// 国际服
ServerGlobalChannelDefaultSubChannelDefaultCompatOnly,
ServerGlobalChannelOfficialSubChannelDefault,
ServerGlobalChannelOfficialSubChannelOfficial,
ServerGlobalChannelOfficialSubChannelEpic,
ServerGlobalChannelOfficialSubChannelGoogle,
};
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Unlocker;
namespace Snap.Hutao.Service.Game;
internal sealed class LaunchStatus
@@ -14,4 +16,16 @@ internal sealed class LaunchStatus
public LaunchPhase Phase { get; set; }
public string Description { get; set; }
}
public static LaunchStatus FromUnlockStatus(UnlockerStatus unlockerStatus)
{
if (unlockerStatus.FindModuleState == FindModuleResult.Ok)
{
return new(LaunchPhase.UnlockFpsSucceed, SH.ServiceGameLaunchPhaseUnlockFpsSucceed);
}
else
{
return new(LaunchPhase.UnlockFpsFailed, SH.ServiceGameLaunchPhaseUnlockFpsFailed);
}
}
}

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.IO;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Factory.Picker;
using Windows.Storage.Pickers;
namespace Snap.Hutao.Service.Game.Locator;

View File

@@ -0,0 +1,67 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using Snap.Hutao.Web.Response;
using System.IO;
using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Package;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGamePackageService))]
internal sealed partial class GamePackageService : IGamePackageService
{
private readonly PackageConverter packageConverter;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
if (!appOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
{
return false;
}
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
Response<GameResource> response = await serviceProvider
.GetRequiredService<ResourceClient>()
.GetResourceAsync(launchScheme)
.ConfigureAwait(false);
if (response.IsOk())
{
GameResource resource = response.Data;
if (!launchScheme.ExecutableMatches(gameFileName))
{
bool replaced = await packageConverter
.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress)
.ConfigureAwait(false);
if (replaced)
{
// We need to change the gamePath if we switched.
string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName;
await taskContext.SwitchToMainThreadAsync();
appOptions.GamePath = Path.Combine(gameFolder, exeName);
}
else
{
// We can't start the game
// when we failed to convert game
return false;
}
}
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Scheme;
namespace Snap.Hutao.Service.Game.Package;
internal interface IGamePackageService
{
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
}

View File

@@ -6,6 +6,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using System.IO;
using System.IO.Compression;

View File

@@ -0,0 +1,135 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Game.Unlocker;
using System.IO;
using Windows.System;
using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Process;
/// <summary>
/// 进程互操作
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGameProcessService))]
internal sealed partial class GameProcessService : IGameProcessService
{
private readonly IServiceProvider serviceProvider;
private readonly RuntimeOptions runtimeOptions;
private readonly LaunchOptions launchOptions;
private readonly AppOptions appOptions;
private volatile int runningGamesCounter;
public bool IsGameRunning()
{
if (runningGamesCounter == 0)
{
return false;
}
return System.Diagnostics.Process.GetProcessesByName(YuanShenProcessName).Any()
|| System.Diagnostics.Process.GetProcessesByName(GenshinImpactProcessName).Any();
}
public async ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
{
if (IsGameRunning())
{
return;
}
string gamePath = appOptions.GamePath;
ArgumentException.ThrowIfNullOrEmpty(gamePath);
progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
using (System.Diagnostics.Process game = InitializeGameProcess(gamePath))
{
try
{
Interlocked.Increment(ref runningGamesCounter);
game.Start();
progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted));
if (launchOptions.UseStarwardPlayTimeStatistics && appOptions.TryGetGameFileName(out string? gameFileName))
{
bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileName);
await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false);
}
if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
{
progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
try
{
await UnlockFpsAsync(game, progress).ConfigureAwait(false);
}
catch (InvalidOperationException)
{
// The Unlocker can't unlock the process
game.Kill();
throw;
}
finally
{
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
}
}
else
{
progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit));
await game.WaitForExitAsync().ConfigureAwait(false);
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
}
}
finally
{
Interlocked.Decrement(ref runningGamesCounter);
}
}
}
private System.Diagnostics.Process InitializeGameProcess(string gamePath)
{
string commandLine = string.Empty;
if (launchOptions.IsEnabled)
{
Must.Argument(!(launchOptions.IsBorderless && launchOptions.IsExclusive), "无边框与独占全屏选项无法同时生效");
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
// https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html
commandLine = new CommandLineBuilder()
.AppendIf("-popupwindow", launchOptions.IsBorderless)
.AppendIf("-window-mode", launchOptions.IsExclusive, "exclusive")
.Append("-screen-fullscreen", launchOptions.IsFullScreen ? 1 : 0)
.AppendIf("-screen-width", launchOptions.IsScreenWidthEnabled, launchOptions.ScreenWidth)
.AppendIf("-screen-height", launchOptions.IsScreenHeightEnabled, launchOptions.ScreenHeight)
.AppendIf("-monitor", launchOptions.IsMonitorEnabled, launchOptions.Monitor.Value)
.ToString();
}
return new()
{
StartInfo = new()
{
Arguments = commandLine,
FileName = gamePath,
UseShellExecute = true,
Verb = "runas",
WorkingDirectory = Path.GetDirectoryName(gamePath),
},
};
}
private ValueTask UnlockFpsAsync(System.Diagnostics.Process game, IProgress<LaunchStatus> progress, CancellationToken token = default)
{
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
UnlockTimingOptions options = new(100, 20000, 3000);
Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus)));
return unlocker.UnlockAsync(options, lockerProgress, token);
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Process;
internal interface IGameProcessService
{
bool IsGameRunning();
ValueTask LaunchAsync(IProgress<LaunchStatus> progress);
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.System;
namespace Snap.Hutao.Service.Game.Process;
internal static class Starward
{
public static async ValueTask LaunchForPlayTimeStatisticsAsync(bool isOversea)
{
string gameBiz = isOversea ? "hk4e_global" : "hk4e_cn";
Uri starwardPlayTimeUri = $"starward://playtime/{gameBiz}".ToUri();
if (await Launcher.QueryUriSupportAsync(starwardPlayTimeUri, LaunchQuerySupportType.Uri) is LaunchQuerySupportStatus.Available)
{
await Launcher.LaunchUriAsync(starwardPlayTimeUri);
}
}
}

View File

@@ -1,157 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Service.Game.Unlocker;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
using Windows.Win32.System.Memory;
using Windows.Win32.System.Threading;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 进程互操作
/// </summary>
internal static class ProcessInterop
{
/// <summary>
/// 获取初始化后的游戏进程
/// </summary>
/// <param name="options">启动选项</param>
/// <param name="gamePath">游戏路径</param>
/// <returns>初始化后的游戏进程</returns>
public static Process InitializeGameProcess(LaunchOptions options, string gamePath)
{
Must.Argument(!(options.IsBorderless && options.IsExclusive), "无边框与独占全屏选项无法同时生效");
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
// https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html
string commandLine = new CommandLineBuilder()
.AppendIf("-popupwindow", options.IsBorderless)
.AppendIf("-window-mode", options.IsExclusive, "exclusive")
.Append("-screen-fullscreen", options.IsFullScreen ? 1 : 0)
.Append("-screen-width", options.ScreenWidth)
.Append("-screen-height", options.ScreenHeight)
.Append("-monitor", options.Monitor.Value)
.ToString();
return new()
{
StartInfo = new()
{
Arguments = commandLine,
FileName = gamePath,
UseShellExecute = true,
Verb = "runas",
WorkingDirectory = Path.GetDirectoryName(gamePath),
},
};
}
public static ValueTask UnlockFpsAsync(IServiceProvider serviceProvider, Process game, IProgress<LaunchStatus> progress, CancellationToken token = default)
{
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
UnlockTimingOptions options = new(100, 20000, 3000);
Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(FromUnlockStatus(unlockStatus)));
return unlocker.UnlockAsync(options, lockerProgress, token);
}
/// <summary>
/// 尝试禁用mhypbase
/// </summary>
/// <param name="game">游戏进程</param>
/// <param name="gamePath">游戏路径</param>
/// <returns>是否禁用成功</returns>
public static bool DisableProtection(Process game, string gamePath)
{
string? gameFolder = Path.GetDirectoryName(gamePath);
string mhypbaseDll = Path.Combine(gameFolder ?? string.Empty, "mhypbase.dll");
if (File.Exists(mhypbaseDll))
{
using (File.OpenHandle(mhypbaseDll, share: FileShare.None))
{
SpinWait.SpinUntil(() => game.MainWindowHandle != 0);
return true;
}
}
return false;
}
/// <summary>
/// 加载并注入指定路径的库
/// </summary>
/// <param name="hProcess">进程句柄</param>
/// <param name="libraryPathu8">库的路径,不包含'\0'</param>
public static unsafe void LoadLibraryAndInject(in HANDLE hProcess, in ReadOnlySpan<byte> libraryPathu8)
{
HINSTANCE hKernelDll = GetModuleHandle("kernel32.dll");
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
FARPROC pLoadLibraryA = GetProcAddress(hKernelDll, "LoadLibraryA"u8);
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
void* pNativeLibraryPath = default;
try
{
VIRTUAL_ALLOCATION_TYPE allocType = VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE | VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT;
pNativeLibraryPath = VirtualAllocEx(hProcess, default, unchecked((uint)libraryPathu8.Length + 1), allocType, PAGE_PROTECTION_FLAGS.PAGE_READWRITE);
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
WriteProcessMemory(hProcess, pNativeLibraryPath, libraryPathu8);
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
LPTHREAD_START_ROUTINE lpThreadLoadLibraryA = pLoadLibraryA.CreateDelegate<LPTHREAD_START_ROUTINE>();
HANDLE hLoadLibraryAThread = default;
try
{
hLoadLibraryAThread = CreateRemoteThread(hProcess, default, 0, lpThreadLoadLibraryA, pNativeLibraryPath, 0);
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
WaitForSingleObject(hLoadLibraryAThread, 2000);
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
}
finally
{
CloseHandle(hLoadLibraryAThread);
}
}
finally
{
VirtualFreeEx(hProcess, pNativeLibraryPath, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE);
}
}
private static unsafe FARPROC GetProcAddress(in HINSTANCE hModule, in ReadOnlySpan<byte> lpProcName)
{
fixed (byte* lpProcNameLocal = lpProcName)
{
return Windows.Win32.PInvoke.GetProcAddress(hModule, new PCSTR(lpProcNameLocal));
}
}
private static unsafe BOOL WriteProcessMemory(in HANDLE hProcess, void* lpBaseAddress, in ReadOnlySpan<byte> buffer)
{
fixed (void* lpBuffer = buffer)
{
return Windows.Win32.PInvoke.WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, unchecked((uint)buffer.Length));
}
}
private static LaunchStatus FromUnlockStatus(UnlockerStatus unlockerStatus)
{
if (unlockerStatus.FindModuleState == FindModuleResult.Ok)
{
return new(LaunchPhase.UnlockFpsSucceed, SH.ServiceGameLaunchPhaseUnlockFpsSucceed);
}
else
{
return new(LaunchPhase.UnlockFpsFailed, SH.ServiceGameLaunchPhaseUnlockFpsFailed);
}
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Service.Game.Scheme;
internal static class KnownLaunchSchemes
{
private static readonly LaunchScheme ServerChineseChannelDefaultSubChannelDefaultCompat = new LaunchSchemeChinese(ChannelType.Default, SubChannelType.Default, false);
private static readonly LaunchScheme ServerChineseChannelOfficialSubChannelDefault = new LaunchSchemeChinese(ChannelType.Official, SubChannelType.Default);
private static readonly LaunchScheme ServerChineseChannelOfficialSubChannelOfficial = new LaunchSchemeChinese(ChannelType.Official, SubChannelType.Official);
private static readonly LaunchScheme ServerChineseChannelOfficialSubChannelNoTapTap = new LaunchSchemeChinese(ChannelType.Official, SubChannelType.NoTapTap);
private static readonly LaunchScheme ServerChineseChannelOfficialSubChannelEpicCompat = new LaunchSchemeChinese(ChannelType.Official, SubChannelType.Epic, false);
private static readonly LaunchScheme ServerChineseChannelBilibiliSubChannelDefault = new LaunchSchemeBilibili(SubChannelType.Default);
private static readonly LaunchScheme ServerChineseChannelBilibiliSubChannelOfficialCompat = new LaunchSchemeBilibili(SubChannelType.Official, false);
private static readonly LaunchScheme ServerGlobalChannelDefaultSubChannelDefaultCompat = new LaunchSchemeOversea(ChannelType.Default, SubChannelType.Default, false);
private static readonly LaunchScheme ServerGlobalChannelOfficialSubChannelDefault = new LaunchSchemeOversea(ChannelType.Official, SubChannelType.Default);
private static readonly LaunchScheme ServerGlobalChannelOfficialSubChannelOfficial = new LaunchSchemeOversea(ChannelType.Official, SubChannelType.Official);
private static readonly LaunchScheme ServerGlobalChannelOfficialSubChannelEpic = new LaunchSchemeOversea(ChannelType.Official, SubChannelType.Epic);
private static readonly LaunchScheme ServerGlobalChannelOfficialSubChannelGoogle = new LaunchSchemeOversea(ChannelType.Official, SubChannelType.Google);
/// <summary>
/// 获取已知的启动方案
/// </summary>
/// <returns>已知的启动方案</returns>
public static List<LaunchScheme> Get()
{
return new List<LaunchScheme>()
{
// 官服
ServerChineseChannelDefaultSubChannelDefaultCompat,
ServerChineseChannelOfficialSubChannelDefault,
ServerChineseChannelOfficialSubChannelOfficial,
ServerChineseChannelOfficialSubChannelNoTapTap,
ServerChineseChannelOfficialSubChannelEpicCompat,
// 渠道服
ServerChineseChannelBilibiliSubChannelDefault,
ServerChineseChannelBilibiliSubChannelOfficialCompat,
// 国际服
ServerGlobalChannelDefaultSubChannelDefaultCompat,
ServerGlobalChannelOfficialSubChannelDefault,
ServerGlobalChannelOfficialSubChannelOfficial,
ServerGlobalChannelOfficialSubChannelEpic,
ServerGlobalChannelOfficialSubChannelGoogle,
};
}
}

View File

@@ -2,14 +2,15 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Service.Game.Configuration;
namespace Snap.Hutao.Service.Game;
namespace Snap.Hutao.Service.Game.Scheme;
/// <summary>
/// 启动方案
/// </summary>
[HighQuality]
internal sealed partial class LaunchScheme
internal class LaunchScheme : IEquatable<ChannelOptions>
{
/// <summary>
/// 显示名称
@@ -32,38 +33,44 @@ internal sealed partial class LaunchScheme
/// <summary>
/// 通道
/// </summary>
public ChannelType Channel { get; private set; }
public ChannelType Channel { get; private protected set; }
/// <summary>
/// 子通道
/// </summary>
public SubChannelType SubChannel { get; private set; }
public SubChannelType SubChannel { get; private protected set; }
/// <summary>
/// 启动器 Id
/// </summary>
public int LauncherId { get; private set; }
public int LauncherId { get; private protected set; }
/// <summary>
/// API Key
/// </summary>
public string Key { get; private set; } = default!;
public string Key { get; private protected set; } = default!;
/// <summary>
/// 是否为海外
/// </summary>
public bool IsOversea { get; private set; }
public bool IsOversea { get; private protected set; }
public bool IsNotCompatOnly { get; private set; } = true;
public bool IsNotCompatOnly { get; private protected set; } = true;
/// <summary>
/// 多通道相等
/// </summary>
/// <param name="multiChannel">多通道</param>
/// <returns>是否相等</returns>
public bool MultiChannelEqual(in ChannelOptions multiChannel)
public static bool ExecutableIsOversea(string gameFileName)
{
return Channel == multiChannel.Channel && SubChannel == multiChannel.SubChannel;
return gameFileName switch
{
GameConstants.GenshinImpactFileName => true,
GameConstants.YuanShenFileName => false,
_ => throw Requires.Fail("无效的游戏可执行文件名称:{0}", gameFileName),
};
}
[SuppressMessage("", "SH002")]
public bool Equals(ChannelOptions other)
{
return Channel == other.Channel && SubChannel == other.SubChannel;
}
public bool ExecutableMatches(string gameFileName)

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Service.Game.Scheme;
internal sealed class LaunchSchemeBilibili : LaunchScheme
{
private const int SdkStaticLauncherBilibiliId = 17;
private const string SdkStaticLauncherBilibiliKey = "KAtdSsoQ";
public LaunchSchemeBilibili(SubChannelType subChannel, bool isNotCompatOnly = true)
{
LauncherId = SdkStaticLauncherBilibiliId;
Key = SdkStaticLauncherBilibiliKey;
Channel = ChannelType.Bili;
SubChannel = subChannel;
IsOversea = false;
IsNotCompatOnly = isNotCompatOnly;
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Service.Game.Scheme;
internal sealed class LaunchSchemeChinese : LaunchScheme
{
private const int SdkStaticLauncherChineseId = 18;
private const string SdkStaticLauncherChineseKey = "eYd89JmJ";
public LaunchSchemeChinese(ChannelType channel, SubChannelType subChannel, bool isNotCompatOnly = true)
{
LauncherId = SdkStaticLauncherChineseId;
Key = SdkStaticLauncherChineseKey;
Channel = channel;
SubChannel = subChannel;
IsOversea = false;
IsNotCompatOnly = isNotCompatOnly;
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Service.Game.Scheme;
internal sealed class LaunchSchemeOversea : LaunchScheme
{
private const int SdkStaticLauncherOverseaId = 10;
private const string SdkStaticLauncherOverseaKey = "gcStgarh";
public LaunchSchemeOversea(ChannelType channel, SubChannelType subChannel, bool isNotCompatOnly = true)
{
LauncherId = SdkStaticLauncherOverseaId;
Key = SdkStaticLauncherOverseaKey;
Channel = channel;
SubChannel = subChannel;
IsOversea = true;
IsNotCompatOnly = isNotCompatOnly;
}
}

View File

@@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.Game.Unlocker;
[HighQuality]
internal sealed class GameFpsUnlocker : IGameFpsUnlocker
{
private readonly Process gameProcess;
private readonly System.Diagnostics.Process gameProcess;
private readonly LaunchOptions launchOptions;
private readonly UnlockerStatus status = new();
@@ -33,7 +33,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="gameProcess">游戏进程</param>
public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess)
public GameFpsUnlocker(IServiceProvider serviceProvider, System.Diagnostics.Process gameProcess)
{
launchOptions = serviceProvider.GetRequiredService<LaunchOptions>();
this.gameProcess = gameProcess;
@@ -57,7 +57,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
await LoopAdjustFpsAsync(options.AdjustFpsDelay, progress, token).ConfigureAwait(false);
}
private static unsafe bool UnsafeReadModulesMemory(Process process, in GameModule moduleEntryInfo, out VirtualMemory memory)
private static unsafe bool UnsafeReadModulesMemory(System.Diagnostics.Process process, in GameModule moduleEntryInfo, out VirtualMemory memory)
{
ref readonly Module unityPlayer = ref moduleEntryInfo.UnityPlayer;
ref readonly Module userAssembly = ref moduleEntryInfo.UserAssembly;
@@ -68,7 +68,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
&& ReadProcessMemory((HANDLE)process.Handle, (void*)userAssembly.Address, lpBuffer + unityPlayer.Size, userAssembly.Size);
}
private static unsafe bool UnsafeReadProcessMemory(Process process, nuint baseAddress, out nuint value)
private static unsafe bool UnsafeReadProcessMemory(System.Diagnostics.Process process, nuint baseAddress, out nuint value)
{
ulong temp = 0;
bool result = ReadProcessMemory((HANDLE)process.Handle, (void*)baseAddress, (byte*)&temp, 8);
@@ -78,7 +78,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
return result;
}
private static unsafe bool UnsafeWriteProcessMemory(Process process, nuint baseAddress, int value)
private static unsafe bool UnsafeWriteProcessMemory(System.Diagnostics.Process process, nuint baseAddress, int value)
{
return WriteProcessMemory((HANDLE)process.Handle, (void*)baseAddress, &value, sizeof(int));
}

View File

@@ -29,11 +29,11 @@ internal sealed partial class HutaoAsAService : IHutaoAsAService
ApplicationDataCompositeValue excludedIds = LocalSetting.Get(SettingKeys.ExcludedAnnouncementIds, new ApplicationDataCompositeValue());
List<long> data = excludedIds.Select(kvp => long.Parse(kvp.Key, CultureInfo.InvariantCulture)).ToList();
Response<List<HutaoAnnouncement>> respose = await hutaoAsServiceClient.GetAnnouncementListAsync(data, token).ConfigureAwait(false);
Response<List<HutaoAnnouncement>> response = await hutaoAsServiceClient.GetAnnouncementListAsync(data, token).ConfigureAwait(false);
if (respose.IsOk())
if (response.IsOk())
{
List<HutaoAnnouncement> list = respose.Data;
List<HutaoAnnouncement> list = response.Data;
list.ForEach(item => item.DismissCommand = dismissCommand);
announcements = list.ToObservableCollection();
}

Some files were not shown because too many files have changed in this diff Show More