mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39831b0ae1 | ||
|
|
51c9936018 | ||
|
|
3466a98ffb | ||
|
|
a064cc10ee | ||
|
|
3479a19164 | ||
|
|
d4549581c1 | ||
|
|
f97bc344d0 | ||
|
|
26d23fec7f | ||
|
|
7442f7f1ec | ||
|
|
3eb2556393 | ||
|
|
cfff6f39fc | ||
|
|
3005031b39 | ||
|
|
71363f4d8d | ||
|
|
e833578334 | ||
|
|
d529b3cea6 | ||
|
|
1c0ce62885 | ||
|
|
acdf2baa9a | ||
|
|
ec007d5d81 | ||
|
|
5e734ac689 | ||
|
|
b0ecd048b6 | ||
|
|
91010d0d8b | ||
|
|
fc771eb90a | ||
|
|
80f2fed722 | ||
|
|
bdb406c451 | ||
|
|
5bc957c6a5 | ||
|
|
416c6f15a6 | ||
|
|
9eed633e05 | ||
|
|
7e30173990 | ||
|
|
2200e2e58e | ||
|
|
b8886c5cd3 | ||
|
|
43007d8fb4 | ||
|
|
88684bff00 | ||
|
|
0c7ce7a72f | ||
|
|
075d92f754 | ||
|
|
a0cba171cc | ||
|
|
f41185310b | ||
|
|
2a4c93d241 | ||
|
|
c0980fabe8 | ||
|
|
f2ba316059 | ||
|
|
e4e9dd91f1 | ||
|
|
749ef0e138 | ||
|
|
24086ee4d0 | ||
|
|
aeb6962ae4 | ||
|
|
87e5ede91f | ||
|
|
91de6d170e | ||
|
|
3057673cdb | ||
|
|
c3ace405ac | ||
|
|
0b48581e65 | ||
|
|
4ab129e4a2 | ||
|
|
13ad36f5b4 | ||
|
|
f026321aa8 | ||
|
|
d1dfdf107b | ||
|
|
59f8895675 | ||
|
|
4cb3d5f03f | ||
|
|
067c7d7c4d | ||
|
|
1cc072ba28 | ||
|
|
0e7afa8efb | ||
|
|
b753728b7e | ||
|
|
df019da891 | ||
|
|
c6435f30eb | ||
|
|
3ac0be4220 | ||
|
|
24f6a33256 | ||
|
|
dc9278eb4f | ||
|
|
4b2c82db62 | ||
|
|
70f30edd7c | ||
|
|
c8e8213df6 | ||
|
|
7cad996902 | ||
|
|
eec47b72c7 | ||
|
|
5943b1a1fb | ||
|
|
9f9a5670bc | ||
|
|
10ba927136 | ||
|
|
07d42cedd1 |
@@ -1,7 +1,7 @@
|
||||
name: 功能请求
|
||||
description: 通过这个议题来向开发团队分享你的想法
|
||||
title: "[Feat]: 在这里填写一个合适的标题"
|
||||
labels: ["功能", "priority:none"]
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
|
||||
@@ -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
55
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
Normal 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
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,11 @@
|
||||
mc:Ignorable="d">
|
||||
|
||||
<cwc:SegmentedItem
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}"
|
||||
Tag="List"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
|
||||
<cwc:SegmentedItem
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentGridView}}"
|
||||
Tag="Grid"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
|
||||
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
21
src/Snap.Hutao/Snap.Hutao/Control/Theme/FlyoutStyle.xaml
Normal file
21
src/Snap.Hutao/Snap.Hutao/Control/Theme/FlyoutStyle.xaml
Normal 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>
|
||||
@@ -3,8 +3,18 @@
|
||||
<x:String x:Key="FontIconContentSetting"></x:String>
|
||||
<x:String x:Key="FontIconContentRefresh"></x:String>
|
||||
<x:String x:Key="FontIconContentDelete"></x:String>
|
||||
<x:String x:Key="FontIconContentChevronRight"></x:String>
|
||||
<x:String x:Key="FontIconContentWarning"></x:String>
|
||||
<x:String x:Key="FontIconContentGame"></x:String>
|
||||
<x:String x:Key="FontIconContentOpenInNewWindow"></x:String>
|
||||
<x:String x:Key="FontIconContentFolder"></x:String>
|
||||
<x:String x:Key="FontIconContentCopy"></x:String>
|
||||
<x:String x:Key="FontIconContentBulletedList"></x:String>
|
||||
<x:String x:Key="FontIconContentCheckList"></x:String>
|
||||
<x:String x:Key="FontIconContentWebsite"></x:String>
|
||||
<x:String x:Key="FontIconContentHomeGroup"></x:String>
|
||||
<x:String x:Key="FontIconContentAsteriskBadge12"></x:String>
|
||||
<x:String x:Key="FontIconContentZipFolder"></x:String>
|
||||
</ResourceDictionary>
|
||||
<x:String x:Key="FontIconContentGridView"></x:String>
|
||||
<x:String x:Key="FontIconContentGiftboxOpen"></x:String>
|
||||
</ResourceDictionary>
|
||||
@@ -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>
|
||||
|
||||
@@ -14,4 +14,10 @@
|
||||
<TransitionCollection x:Key="ReorderThemeTransitions">
|
||||
<ReorderThemeTransition/>
|
||||
</TransitionCollection>
|
||||
<TransitionCollection x:Key="RepositionThemeTransitions">
|
||||
<RepositionThemeTransition/>
|
||||
</TransitionCollection>
|
||||
<TransitionCollection x:Key="NavigationThemeTransitions">
|
||||
<NavigationThemeTransition/>
|
||||
</TransitionCollection>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,8 @@ namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
internal interface ICurrentWindowReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Only set in WindowController
|
||||
/// </summary>
|
||||
public Window Window { get; set; }
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Threading;
|
||||
/// </summary>
|
||||
internal interface ITaskContext
|
||||
{
|
||||
IProgress<T> CreateProgressForMainThread<T>(Action<T> handler);
|
||||
SynchronizationContext GetSynchronizationContext();
|
||||
|
||||
/// <summary>
|
||||
/// 在主线程上同步等待执行操作
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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*)¤t);
|
||||
|
||||
UnregisterForCurrentWindow();
|
||||
RegisterForCurrentWindow();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
src/Snap.Hutao/Snap.Hutao/Extension/UnsafeDateTimeOffset.cs
Normal file
20
src/Snap.Hutao/Snap.Hutao/Extension/UnsafeDateTimeOffset.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Windows.Storage.Pickers;
|
||||
|
||||
namespace Snap.Hutao.Factory.Abstraction;
|
||||
namespace Snap.Hutao.Factory.Picker;
|
||||
|
||||
/// <summary>
|
||||
/// 文件选择器工厂
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
549
src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.Designer.cs
generated
Normal file
549
src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
13
src/Snap.Hutao/Snap.Hutao/Model/CollectionsNameValue.cs
Normal file
13
src/Snap.Hutao/Snap.Hutao/Model/CollectionsNameValue.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,17 @@ internal enum Arkhe
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// 芒性
|
||||
/// 荒性
|
||||
/// </summary>
|
||||
Ousia,
|
||||
|
||||
/// <summary>
|
||||
/// 荒性
|
||||
/// 芒性
|
||||
/// </summary>
|
||||
Pneuma,
|
||||
|
||||
/// <summary>
|
||||
/// 圣俗杂座
|
||||
/// </summary>
|
||||
Furina,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -19,6 +19,8 @@ internal static class MonsterRelationship
|
||||
5071U => 507U, // 幻形花鼠 · 水 (强化)
|
||||
5102U => 510U, // 历经百战的浊水粉碎幻灵
|
||||
5112U => 511U, // 历经百战的浊水喷吐幻灵
|
||||
30605U => 30603U, // 历经百战的霜剑律从
|
||||
30606U => 30604U, // 历经百战的幽风铃兰
|
||||
60402U => 60401U, // (火)岩龙蜥
|
||||
60403U => 60401U, // (冰)岩龙蜥
|
||||
60404U => 60401U, // (雷)岩龙蜥
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
42
src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs
Normal file
42
src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,7 @@ internal sealed class TypedWishSummaryBuilder
|
||||
{
|
||||
// base
|
||||
Name = context.Name,
|
||||
TypeName = $"{context.DistributionType:D}",
|
||||
From = fromTimeTracker,
|
||||
To = toTimeTracker,
|
||||
TotalCount = totalCountTracker,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
namespace Snap.Hutao.Service.Game.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 多通道
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
60
src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs
Normal file
60
src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs
Normal 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!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
105
src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs
Normal file
105
src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
/// 游戏内账号集合
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
19
src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs
Normal file
19
src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user