mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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:
|
||||
|
||||
@@ -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,275 @@
|
||||
// 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)
|
||||
{
|
||||
// Item has not been measured yet. Get the element and store the values
|
||||
UIElement element = context.GetOrCreateElementAt(i);
|
||||
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}";
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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/>
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Threading;
|
||||
/// </summary>
|
||||
internal interface ITaskContext
|
||||
{
|
||||
IProgress<T> CreateProgressForMainThread<T>(Action<T> handler);
|
||||
SynchronizationContext GetSynchronizationContext();
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
@@ -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,10 @@
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Windows.Storage.Pickers;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Snap.Hutao.Factory;
|
||||
namespace Snap.Hutao.Factory.Picker;
|
||||
|
||||
/// <inheritdoc cref="IPickerFactory"/>
|
||||
[HighQuality]
|
||||
@@ -18,7 +17,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 +79,11 @@ 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);
|
||||
}
|
||||
nint hwnd = currentWindowReference.Window is IWindowOptionsSource optionsSource
|
||||
? (nint)optionsSource.WindowOptions.Hwnd
|
||||
: WindowNative.GetWindowHandle(currentWindowReference.Window);
|
||||
|
||||
InitializeWithWindow.Initialize(picker, hwnd);
|
||||
|
||||
return picker;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Snap.Hutao.Factory.Progress;
|
||||
|
||||
internal interface IProgressFactory
|
||||
{
|
||||
IProgress<T> CreateForMainThread<T>(Action<T> handler);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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");
|
||||
|
||||
|
||||
@@ -53,6 +53,11 @@ internal sealed partial class SettingEntry
|
||||
/// </summary>
|
||||
public const string DailyNoteWebhookUrl = "DailyNote.WebhookUrl";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 总开关
|
||||
/// </summary>
|
||||
public const string LaunchIsLaunchOptionsEnabled = "Launch.IsLaunchOptionsEnabled";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 独占全屏
|
||||
/// </summary>
|
||||
@@ -99,6 +104,8 @@ internal sealed partial class SettingEntry
|
||||
|
||||
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>
|
||||
|
||||
@@ -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, // (雷)岩龙蜥
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutao"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.7.15.0" />
|
||||
Version="1.7.16.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>
|
||||
@@ -2486,6 +2510,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -2486,6 +2510,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>
|
||||
@@ -2622,7 +2649,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -2486,6 +2510,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>
|
||||
|
||||
@@ -1967,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>
|
||||
@@ -2015,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>
|
||||
@@ -2030,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>
|
||||
@@ -2726,6 +2741,9 @@
|
||||
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
|
||||
<value>武器活动祈愿</value>
|
||||
</data>
|
||||
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
|
||||
<value>下载链接复制成功</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>
|
||||
@@ -2486,6 +2510,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>
|
||||
|
||||
@@ -106,6 +106,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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,6 +24,7 @@ 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;
|
||||
@@ -34,6 +36,8 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
private int? targetFps;
|
||||
private NameValue<int>? monitor;
|
||||
private bool? isMonitorEnabled;
|
||||
private AspectRatio? selectedAspectRatio;
|
||||
private bool? useStarwardPlayTimeStatistics;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的启动游戏选项
|
||||
@@ -50,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>
|
||||
@@ -152,6 +165,31 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
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,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)
|
||||
.AppendIf("-screen-width", options.IsScreenWidthEnabled, options.ScreenWidth)
|
||||
.AppendIf("-screen-height", options.IsScreenHeightEnabled, options.ScreenHeight)
|
||||
.AppendIf("-monitor", options.IsMonitorEnabled, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
namespace Snap.Hutao.Service.Game.Scheme;
|
||||
|
||||
internal static class KnownLaunchSchemes
|
||||
{
|
||||
@@ -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 partial class LaunchScheme
|
||||
internal class LaunchScheme
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示名称
|
||||
@@ -56,6 +57,16 @@ internal partial class LaunchScheme
|
||||
|
||||
public bool IsNotCompatOnly { get; private protected set; } = true;
|
||||
|
||||
public static bool ExecutableIsOversea(string gameFileName)
|
||||
{
|
||||
return gameFileName switch
|
||||
{
|
||||
GameConstants.GenshinImpactFileName => true,
|
||||
GameConstants.YuanShenFileName => false,
|
||||
_ => throw Requires.Fail("无效的游戏可执行文件名称:{0}", gameFileName),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多通道相等
|
||||
/// </summary>
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
namespace Snap.Hutao.Service.Game.Scheme;
|
||||
|
||||
internal sealed class LaunchSchemeBilibili : LaunchScheme
|
||||
{
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
namespace Snap.Hutao.Service.Game.Scheme;
|
||||
|
||||
internal sealed class LaunchSchemeChinese : LaunchScheme
|
||||
{
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
namespace Snap.Hutao.Service.Game.Scheme;
|
||||
|
||||
internal sealed class LaunchSchemeOversea : LaunchScheme
|
||||
{
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.User;
|
||||
|
||||
internal interface IUserFingerprintService
|
||||
{
|
||||
ValueTask TryInitializeAsync(ViewModel.User.User user, CancellationToken token = default);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.User;
|
||||
|
||||
internal interface IUserInitializationService
|
||||
{
|
||||
ValueTask<ViewModel.User.User?> CreateOrDefaultUserFromCookieAsync(Cookie cookie, bool isOversea, CancellationToken token = default(CancellationToken));
|
||||
ValueTask<ViewModel.User.User?> CreateUserFromCookieOrDefaultAsync(Cookie cookie, bool isOversea, CancellationToken token = default(CancellationToken));
|
||||
|
||||
ValueTask<ViewModel.User.User> ResumeUserAsync(Model.Entity.User inner, CancellationToken token = default(CancellationToken));
|
||||
}
|
||||
113
src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs
Normal file
113
src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.PublicData.DeviceFp;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Service.User;
|
||||
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IUserFingerprintService))]
|
||||
internal sealed partial class UserFingerprintService : IUserFingerprintService
|
||||
{
|
||||
private readonly DeviceFpClient deviceFpClient;
|
||||
|
||||
public async ValueTask TryInitializeAsync(ViewModel.User.User user, CancellationToken token = default)
|
||||
{
|
||||
if (user.IsOversea)
|
||||
{
|
||||
// disable HoYoLAB fp approach
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(user.Fingerprint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string model = GetRandomStringOfLength(6);
|
||||
Dictionary<string, string> extendProperties = new()
|
||||
{
|
||||
{ "cpuType", "arm64-v8a" },
|
||||
{ "romCapacity", "512" },
|
||||
{ "productName", model },
|
||||
{ "romRemain", "256" },
|
||||
{ "manufacturer", "XiaoMi" },
|
||||
{ "appMemory", "512" },
|
||||
{ "hostname", "dg02-pool03-kvm87" },
|
||||
{ "screenSize", "1080x1920" },
|
||||
{ "osVersion", "13" },
|
||||
{ "aaid", string.Empty },
|
||||
{ "vendor", "中国移动" },
|
||||
{ "accelerometer", "1.4883357x7.1712894x6.2847486" },
|
||||
{ "buildTags", "release-keys" },
|
||||
{ "model", model },
|
||||
{ "brand", "XiaoMi" },
|
||||
{ "oaid", string.Empty },
|
||||
{ "hardware", "qcom" },
|
||||
{ "deviceType", "OP5913L1" },
|
||||
{ "devId", "REL" },
|
||||
{ "serialNumber", "unknown" },
|
||||
{ "buildTime", "1687848011000" },
|
||||
{ "buildUser", "root" },
|
||||
{ "ramCapacity", "469679" },
|
||||
{ "magnetometer", "20.081251x-27.487501x2.1937501" },
|
||||
{ "display", $"{model}_13.1.0.181(CN01)" },
|
||||
{ "ramRemain", "215344" },
|
||||
{ "deviceInfo", $@"XiaoMi/{model}/OP5913L1:13/SKQ1.221119.001/T.118e6c7-5aa23-73911:user/release-keys" },
|
||||
{ "gyroscope", "0.030226856x0.014647375x0.010652636" },
|
||||
{ "vaid", string.Empty },
|
||||
{ "buildType", "user" },
|
||||
{ "sdkVersion", "33" },
|
||||
{ "board", "taro" },
|
||||
};
|
||||
|
||||
DeviceFpData data = new()
|
||||
{
|
||||
DeviceId = GetRandomHexStringOfLength(16),
|
||||
SeedId = $"{Guid.NewGuid()}",
|
||||
Platform = "2",
|
||||
SeedTime = $"{DateTimeOffset.Now.ToUnixTimeMilliseconds()}",
|
||||
ExtFields = JsonSerializer.Serialize(extendProperties),
|
||||
AppName = "bbs_cn",
|
||||
BbsDeviceId = HoyolabOptions.DeviceId,
|
||||
DeviceFp = string.IsNullOrEmpty(user.Fingerprint) ? GetRandomHexStringOfLength(13) : user.Fingerprint,
|
||||
};
|
||||
|
||||
Response<DeviceFpWrapper> response = await deviceFpClient.GetFingerprintAsync(data, token).ConfigureAwait(false);
|
||||
user.Fingerprint = response.IsOk() ? response.Data.DeviceFp : string.Empty;
|
||||
user.NeedDbUpdateAfterResume = true;
|
||||
}
|
||||
|
||||
private static string GetRandomHexStringOfLength(int length)
|
||||
{
|
||||
const string RandomRange = "0123456789abcdef";
|
||||
|
||||
StringBuilder sb = new(length);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
int pos = Random.Shared.Next(0, RandomRange.Length);
|
||||
sb.Append(RandomRange[pos]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GetRandomStringOfLength(int length)
|
||||
{
|
||||
const string RandomRange = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
StringBuilder sb = new(length);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
int pos = Random.Shared.Next(0, RandomRange.Length);
|
||||
sb.Append(RandomRange[pos]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ namespace Snap.Hutao.Service.User;
|
||||
[Injection(InjectAs.Singleton, typeof(IUserInitializationService))]
|
||||
internal sealed partial class UserInitializationService : IUserInitializationService
|
||||
{
|
||||
private readonly IUserFingerprintService userFingerprintService;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
public async ValueTask<ViewModel.User.User> ResumeUserAsync(Model.Entity.User inner, CancellationToken token = default)
|
||||
@@ -29,7 +30,7 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
|
||||
return user;
|
||||
}
|
||||
|
||||
public async ValueTask<ViewModel.User.User?> CreateOrDefaultUserFromCookieAsync(Cookie cookie, bool isOversea, CancellationToken token = default)
|
||||
public async ValueTask<ViewModel.User.User?> CreateUserFromCookieOrDefaultAsync(Cookie cookie, bool isOversea, CancellationToken token = default)
|
||||
{
|
||||
// 这里只负责创建实体用户,稍后在用户服务中保存到数据库
|
||||
Model.Entity.User entity = Model.Entity.User.From(cookie, isOversea);
|
||||
@@ -64,8 +65,6 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isOversea = user.Entity.IsOversea;
|
||||
|
||||
if (!await TrySetUserLTokenAsync(user, token).ConfigureAwait(false))
|
||||
{
|
||||
return false;
|
||||
@@ -86,6 +85,8 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
|
||||
return false;
|
||||
}
|
||||
|
||||
await userFingerprintService.TryInitializeAsync(user, token).ConfigureAwait(false);
|
||||
|
||||
user.SelectedUserGameRole = user.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
|
||||
return user.IsInitialized = true;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,16 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
|
||||
{
|
||||
List<Model.Entity.User> entities = await userDbService.GetUserListAsync().ConfigureAwait(false);
|
||||
List<BindingUser> users = await entities.SelectListAsync(userInitializationService.ResumeUserAsync, default).ConfigureAwait(false);
|
||||
|
||||
foreach (BindingUser user in users)
|
||||
{
|
||||
if (user.NeedDbUpdateAfterResume)
|
||||
{
|
||||
await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false);
|
||||
user.NeedDbUpdateAfterResume = false;
|
||||
}
|
||||
}
|
||||
|
||||
userCollection = users.ToObservableCollection();
|
||||
|
||||
try
|
||||
@@ -202,7 +212,7 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
|
||||
private async ValueTask<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
BindingUser? newUser = await userInitializationService.CreateOrDefaultUserFromCookieAsync(cookie, isOversea).ConfigureAwait(false);
|
||||
BindingUser? newUser = await userInitializationService.CreateUserFromCookieOrDefaultAsync(cookie, isOversea).ConfigureAwait(false);
|
||||
|
||||
if (newUser is not null)
|
||||
{
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.6.11" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.756" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231008000" />
|
||||
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.507">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="{ThemeResource TitleTextBlockFontSize}"
|
||||
Glyph=""/>
|
||||
Glyph="{StaticResource FontIconContentGame}"/>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Background="{ThemeResource SystemFillColorCriticalBackgroundBrush}"
|
||||
Content="{shcm:ResourceString Name=ViewControlElevationText}"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentWarning}}"
|
||||
mc:Ignorable="d">
|
||||
<clw:TokenItem.Resources>
|
||||
<x:Double x:Key="ListViewItemDisabledThemeOpacity">1</x:Double>
|
||||
|
||||
@@ -48,9 +48,9 @@
|
||||
Height="38.4"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
NavigateUri="{Binding Path}"/>
|
||||
Command="{Binding CopyPathCommand}"
|
||||
Content="{StaticResource FontIconContentCopy}"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</Expander.Resources>
|
||||
@@ -92,9 +92,9 @@
|
||||
Height="38.4"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
NavigateUri="{Binding Path}"/>
|
||||
Command="{Binding CopyPathCommand}"
|
||||
Content="{StaticResource FontIconContentCopy}"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
|
||||
<MenuFlyoutSeparator Grid.Row="2" Margin="4,16,4,0"/>
|
||||
<ItemsControl
|
||||
Grid.Row="3"
|
||||
|
||||
@@ -151,6 +151,8 @@
|
||||
x:Name="ItemsPanelSelector"
|
||||
Margin="6,0,0,0"
|
||||
Current="Grid"
|
||||
LocalSettingKeyExtraForCurrent="{Binding TypeName}"
|
||||
LocalSettingKeySuffixForCurrent="StatisticsCard.OrangeList"
|
||||
Visibility="{x:Bind DetailExpander.IsExpanded, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
CompactPaneLength="48"
|
||||
IsBackEnabled="{x:Bind ContentFrame.CanGoBack, Mode=OneWay}"
|
||||
IsPaneOpen="True"
|
||||
OpenPaneLength="188"
|
||||
OpenPaneLength="192"
|
||||
PaneDisplayMode="Left"
|
||||
UseLayoutRounding="False">
|
||||
<NavigationView.MenuItems>
|
||||
|
||||
@@ -133,9 +133,10 @@
|
||||
<TextBlock
|
||||
FontSize="10"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding Inner.Version}"/>
|
||||
Text="{Binding Inner.Version}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"/>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
@@ -201,7 +202,10 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<shcp:PanelSelector x:Name="ItemsPanelSelector" Margin="8,0,0,0"/>
|
||||
<shcp:PanelSelector
|
||||
x:Name="ItemsPanelSelector"
|
||||
Margin="8,0,0,0"
|
||||
LocalSettingKeySuffixForCurrent="AchievementPage.AchievementGoals"/>
|
||||
<Viewbox
|
||||
Height="32"
|
||||
MaxWidth="192"
|
||||
|
||||
@@ -253,6 +253,67 @@
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="AvatarSkillTemplate">
|
||||
<Button
|
||||
Margin="0,2,0,0"
|
||||
Padding="4"
|
||||
Background="Transparent"
|
||||
BorderBrush="{x:Null}">
|
||||
<Button.Content>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition Width="48"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="36"
|
||||
Height="36"
|
||||
Source="{Binding Icon}"/>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="6,0,0,2"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#FFFFFFFF"
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{Binding Info.Level}"/>
|
||||
</Grid>
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="Left">
|
||||
<StackPanel MaxWidth="320">
|
||||
<StackPanel.Resources>
|
||||
<Thickness x:Key="SettingsCardPadding">16</Thickness>
|
||||
<x:Double x:Key="SettingsCardWrapThreshold">0</x:Double>
|
||||
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
|
||||
<x:Double x:Key="SettingsCardMinHeight">0</x:Double>
|
||||
</StackPanel.Resources>
|
||||
<shct:DescriptionTextBlock Description="{Binding Description}">
|
||||
<shct:DescriptionTextBlock.Resources>
|
||||
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
</Style>
|
||||
</shct:DescriptionTextBlock.Resources>
|
||||
</shct:DescriptionTextBlock>
|
||||
<ItemsControl Margin="0,12,0,0" ItemsSource="{Binding Info.Parameters}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<cwcont:SettingsCard
|
||||
Margin="0,2,0,0"
|
||||
Padding="12,0"
|
||||
Header="{Binding Description}">
|
||||
<TextBlock Margin="0,8" Text="{Binding Parameter}"/>
|
||||
</cwcont:SettingsCard>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="AvatarPropertyTemplate">
|
||||
<Grid Padding="16,8" Background="{Binding Background, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -451,7 +512,10 @@
|
||||
DefaultLabelPosition="Right">
|
||||
<CommandBar.Content>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<shcp:PanelSelector x:Name="ItemsPanelSelector" Margin="6,8,0,0"/>
|
||||
<shcp:PanelSelector
|
||||
x:Name="ItemsPanelSelector"
|
||||
Margin="6,8,0,0"
|
||||
LocalSettingKeySuffixForCurrent="AvatarPropertyPage.Summary.Avatars"/>
|
||||
<ToggleButton
|
||||
x:Name="RefreshTimeToggle"
|
||||
Margin="8,8,0,0"
|
||||
@@ -659,44 +723,9 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="16"
|
||||
ItemTemplate="{StaticResource AvatarConstellationTemplate}"
|
||||
ItemsPanel="{StaticResource HorizontalStackPanelSpacing0Template}"
|
||||
ItemsSource="{Binding SelectedAvatar.Constellations}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button
|
||||
Margin="0,0,2,0"
|
||||
Padding="2"
|
||||
Background="Transparent"
|
||||
BorderBrush="{x:Null}">
|
||||
<Button.Content>
|
||||
<Grid>
|
||||
<shci:CachedImage
|
||||
Width="36"
|
||||
Height="36"
|
||||
Opacity="{Binding IsActivated, Converter={StaticResource BoolToOpacityConverter}}"
|
||||
Source="{Binding Icon}"/>
|
||||
<Image
|
||||
Width="16"
|
||||
Height="16"
|
||||
Source="ms-appx:///Resource/Icon/UI_Icon_Locked.png"
|
||||
Visibility="{Binding IsActivated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
</Grid>
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="Bottom">
|
||||
<shct:DescriptionTextBlock MaxWidth="320" Description="{Binding Description}">
|
||||
<shct:DescriptionTextBlock.Resources>
|
||||
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
</Style>
|
||||
</shct:DescriptionTextBlock.Resources>
|
||||
</shct:DescriptionTextBlock>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
ItemsSource="{Binding SelectedAvatar.Constellations}"/>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="16">
|
||||
<TextBlock
|
||||
@@ -729,7 +758,7 @@
|
||||
Grid.Column="1"
|
||||
Margin="16"
|
||||
VerticalAlignment="Bottom"
|
||||
ItemTemplate="{StaticResource AvatarConstellationTemplate}"
|
||||
ItemTemplate="{StaticResource AvatarSkillTemplate}"
|
||||
ItemsSource="{Binding SelectedAvatar.Skills}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
xmlns:shc="using:Snap.Hutao.Control"
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
xmlns:shci="using:Snap.Hutao.Control.Image"
|
||||
xmlns:shcl="using:Snap.Hutao.Control.Layout"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
xmlns:shvco="using:Snap.Hutao.View.Control"
|
||||
xmlns:shvcu="using:Snap.Hutao.ViewModel.Cultivation"
|
||||
@@ -149,10 +150,7 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<MenuFlyoutSeparator Grid.Row="1"/>
|
||||
<ScrollViewer
|
||||
Grid.Row="2"
|
||||
Height="296"
|
||||
Margin="0,2,0,0">
|
||||
<ScrollViewer Grid.Row="2" Margin="0,2,0,0">
|
||||
<ItemsControl
|
||||
Margin="8,0,8,8"
|
||||
ItemTemplate="{StaticResource CultivateEntryItemTemplate}"
|
||||
@@ -280,10 +278,11 @@
|
||||
ItemTemplate="{StaticResource CultivateEntryTemplate}"
|
||||
ItemsSource="{Binding CultivateEntries}"
|
||||
SelectionMode="None">
|
||||
<ItemsView.ItemTransitionProvider>
|
||||
<shcl:DefaultItemCollectionTransitionProvider/>
|
||||
</ItemsView.ItemTransitionProvider>
|
||||
<ItemsView.Layout>
|
||||
<UniformGridLayout
|
||||
ItemsJustification="Start"
|
||||
ItemsStretch="Fill"
|
||||
<shcl:UniformStaggeredLayout
|
||||
MinColumnSpacing="12"
|
||||
MinItemWidth="300"
|
||||
MinRowSpacing="-4"/>
|
||||
|
||||
@@ -382,7 +382,7 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewControlStatisticsCardOrangeText}"/>
|
||||
<GridView
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
|
||||
ItemsSource="{Binding Statistics.OrangeAvatars}"
|
||||
SelectionMode="None"/>
|
||||
|
||||
@@ -391,7 +391,7 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewControlStatisticsCardPurpleText}"/>
|
||||
<GridView
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
|
||||
ItemsSource="{Binding Statistics.PurpleAvatars}"
|
||||
SelectionMode="None"/>
|
||||
</StackPanel>
|
||||
@@ -405,7 +405,7 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewControlStatisticsCardOrangeText}"/>
|
||||
<GridView
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
|
||||
ItemsSource="{Binding Statistics.OrangeWeapons}"
|
||||
SelectionMode="None"/>
|
||||
|
||||
@@ -414,7 +414,7 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewControlStatisticsCardPurpleText}"/>
|
||||
<GridView
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
|
||||
ItemsSource="{Binding Statistics.PurpleWeapons}"
|
||||
SelectionMode="None"/>
|
||||
|
||||
@@ -423,7 +423,7 @@
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewControlStatisticsCardBlueText}"/>
|
||||
<GridView
|
||||
ItemTemplate="{StaticResource HistoryWishItemTemplate}"
|
||||
ItemTemplate="{StaticResource HistoryWishGridTemplate}"
|
||||
ItemsSource="{Binding Statistics.BlueWeapons}"
|
||||
SelectionMode="None"/>
|
||||
</StackPanel>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
xmlns:shc="using:Snap.Hutao.Control"
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
xmlns:shccs="using:Snap.Hutao.Control.Collection.Selector"
|
||||
xmlns:shch="using:Snap.Hutao.Control.Helper"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Control"
|
||||
xmlns:shvg="using:Snap.Hutao.ViewModel.Game"
|
||||
@@ -34,6 +35,82 @@
|
||||
DataContext="{Binding Mode=OneWay}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceDiffHeader}"/>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="GameAccountListTemplate">
|
||||
<Grid>
|
||||
<StackPanel Margin="0,12">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
<TextBlock
|
||||
Opacity="0.8"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding AttachUid, TargetNullValue={shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountAttachUidNull}}"/>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
x:Name="ButtonPanel"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Visibility="Collapsed">
|
||||
<Button
|
||||
MinWidth="48"
|
||||
Margin="4,8"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{Binding DataContext.AttachGameAccountCommand, Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountAttachUidToolTip}"/>
|
||||
<Button
|
||||
MinWidth="48"
|
||||
Margin="4,8"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{Binding DataContext.ModifyGameAccountCommand, Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountRenameToolTip}"/>
|
||||
<Button
|
||||
MinWidth="48"
|
||||
Margin="4,8,0,8"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{Binding DataContext.RemoveGameAccountCommand, Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountRemoveToolTip}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid.Resources>
|
||||
<Storyboard x:Name="ButtonPanelVisibleStoryboard">
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonPanel" Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<Visibility>Visible</Visibility>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
|
||||
<Storyboard x:Name="ButtonPanelCollapsedStoryboard">
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonPanel" Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<Visibility>Collapsed</Visibility>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</Grid.Resources>
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<mxic:EventTriggerBehavior EventName="PointerEntered">
|
||||
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelVisibleStoryboard}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
<mxic:EventTriggerBehavior EventName="PointerExited">
|
||||
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelCollapsedStoryboard}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
</mxi:Interaction.Behaviors>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
<Grid>
|
||||
<Grid
|
||||
@@ -77,6 +154,7 @@
|
||||
Message="{shcm:ResourceString Name=ViewPageLaunchGameConfigurationSaveHint}"
|
||||
Severity="Informational"/>
|
||||
|
||||
<!-- 文件 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameFileHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeHeader}"
|
||||
@@ -103,6 +181,7 @@
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
|
||||
<!-- 注册表 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameRegistryHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
ActionIconToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountDetectAction}"
|
||||
@@ -112,132 +191,70 @@
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsClickEnabled="True"/>
|
||||
<Border Style="{StaticResource BorderCardStyle}">
|
||||
<ListView ItemsSource="{Binding GameAccounts}" SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<StackPanel Margin="0,12">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
<TextBlock
|
||||
Opacity="0.8"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding AttachUid, TargetNullValue={shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountAttachUidNull}}"/>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
x:Name="ButtonPanel"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Visibility="Collapsed">
|
||||
<Button
|
||||
MinWidth="48"
|
||||
Margin="4,8"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{Binding DataContext.AttachGameAccountCommand, Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountAttachUidToolTip}"/>
|
||||
<Button
|
||||
MinWidth="48"
|
||||
Margin="4,8"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{Binding DataContext.ModifyGameAccountCommand, Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountRenameToolTip}"/>
|
||||
<Button
|
||||
MinWidth="48"
|
||||
Margin="4,8,0,8"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{Binding DataContext.RemoveGameAccountCommand, Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountRemoveToolTip}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid.Resources>
|
||||
<Storyboard x:Name="ButtonPanelVisibleStoryboard">
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonPanel" Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<Visibility>Visible</Visibility>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
|
||||
<Storyboard x:Name="ButtonPanelCollapsedStoryboard">
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonPanel" Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<Visibility>Collapsed</Visibility>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</Grid.Resources>
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<mxic:EventTriggerBehavior EventName="PointerEntered">
|
||||
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelVisibleStoryboard}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
<mxic:EventTriggerBehavior EventName="PointerExited">
|
||||
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelCollapsedStoryboard}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
</mxi:Interaction.Behaviors>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<ListView
|
||||
ItemTemplate="{StaticResource GameAccountListTemplate}"
|
||||
ItemsSource="{Binding GameAccounts}"
|
||||
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
|
||||
</Border>
|
||||
|
||||
<!-- 进程 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameProcessHeader}"/>
|
||||
<cwc:SettingsExpander
|
||||
shch:SettingsExpanderHelper.IsItemsEnabled="{Binding Options.IsEnabled}"
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding Options.IsEnabled, Mode=TwoWay}"/>
|
||||
<cwc:SettingsExpander.Items>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceExclusiveDescription}" Header="-window-mode exclusive">
|
||||
<ToggleSwitch Width="156" IsOn="{Binding Options.IsExclusive, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding Options.IsExclusive, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceFullscreenDescription}" Header="-screen-fullscreen">
|
||||
<ToggleSwitch Width="156" IsOn="{Binding Options.IsFullScreen, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding Options.IsFullScreen, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceBorderlessDescription}" Header="-popupwindow">
|
||||
<ToggleSwitch Width="156" IsOn="{Binding Options.IsBorderless, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding Options.IsBorderless, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioHeader}">
|
||||
<ComboBox
|
||||
Width="156"
|
||||
Margin="0,0,136,0"
|
||||
ItemsSource="{Binding Options.AspectRatios}"
|
||||
SelectedItem="{Binding Options.SelectedAspectRatio, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenWidthDescription}" Header="-screen-width">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<NumberBox
|
||||
Width="156"
|
||||
Padding="10,6,0,0"
|
||||
Padding="12,6,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding Options.IsScreenWidthEnabled}"
|
||||
Value="{Binding Options.ScreenWidth, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="156" IsOn="{Binding Options.IsScreenWidthEnabled, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding Options.IsScreenWidthEnabled, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenHeightDescription}" Header="-screen-height">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<NumberBox
|
||||
Width="156"
|
||||
Padding="10,6,0,0"
|
||||
Padding="12,6,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding Options.IsScreenHeightEnabled}"
|
||||
Value="{Binding Options.ScreenHeight, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="156" IsOn="{Binding Options.IsScreenHeightEnabled, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding Options.IsScreenHeightEnabled, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameMonitorsDescription}" Header="-monitor">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<ComboBox
|
||||
Width="156"
|
||||
VerticalAlignment="Center"
|
||||
DisplayMemberPath="Name"
|
||||
IsEnabled="{Binding Options.IsMonitorEnabled}"
|
||||
ItemsSource="{Binding Options.Monitors}"
|
||||
SelectedItem="{Binding Options.Monitor, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="156" IsOn="{Binding Options.IsMonitorEnabled, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding Options.IsMonitorEnabled, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
</cwc:SettingsExpander.Items>
|
||||
@@ -265,8 +282,14 @@
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
|
||||
|
||||
<StackPanel Spacing="{StaticResource SettingsCardSpacing}"/>
|
||||
<!-- 进程间 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameInterProcessHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding Options.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -94,63 +94,68 @@
|
||||
<cwc:SettingsCard Description="{Binding HutaoOptions.WebView2Version}" Header="{shcm:ResourceString Name=ViewPageSettingWebview2Header}"/>
|
||||
</cwc:SettingsExpander.Items>
|
||||
</cwc:SettingsExpander>
|
||||
<cwc:SettingsExpander
|
||||
Description="{Binding UserOptions.UserName}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<Button
|
||||
Command="{Binding Passport.LoginCommand}"
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLoginAction}"
|
||||
Style="{ThemeResource SettingButtonStyle}"
|
||||
Visibility="{Binding UserOptions.IsLoggedIn, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
<Button
|
||||
Command="{Binding Passport.RegisterCommand}"
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingHutaoPassportRegisterAction}"
|
||||
Style="{ThemeResource SettingButtonStyle}"
|
||||
Visibility="{Binding UserOptions.IsLoggedIn, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
<Button
|
||||
Command="{Binding Passport.LogoutCommand}"
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLogoutAction}"
|
||||
Style="{ThemeResource SettingButtonStyle}"
|
||||
Visibility="{Binding UserOptions.IsLoggedIn, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
<cwc:SettingsExpander.Items>
|
||||
<cwc:SettingsCard
|
||||
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
|
||||
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperHeader}"
|
||||
Visibility="{Binding UserOptions.IsLicensedDeveloper, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<cwc:SettingsCard
|
||||
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
|
||||
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerHeader}"
|
||||
Visibility="{Binding UserOptions.IsMaintainer, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<cwc:SettingsCard Description="{Binding UserOptions.GachaLogExpireAtSlim}" Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportGachaLogExpiredAtHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
Command="{Binding Passport.OpenRedeemWebsiteCommand}"
|
||||
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportRedeemCodeDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportRedeemCodeHeader}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwc:SettingsCard
|
||||
Background="{ThemeResource SystemFillColorCriticalBackgroundBrush}"
|
||||
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportDangerZoneDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportDangerZoneHeader}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<Button
|
||||
Command="{Binding Passport.ResetPasswordCommand}"
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingHutaoPassportResetPasswordAction}"
|
||||
Style="{ThemeResource SettingButtonStyle}"/>
|
||||
<Button
|
||||
Command="{Binding Passport.UnregisterCommand}"
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingHutaoPassportUnregisterAction}"
|
||||
Style="{ThemeResource SettingButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
</cwc:SettingsExpander.Items>
|
||||
</cwc:SettingsExpander>
|
||||
|
||||
<!--
|
||||
https://github.com/DGP-Studio/Snap.Hutao/issues/1072
|
||||
ItemsRepeater will behave abnormal is no direct scrollhost wrapping it
|
||||
-->
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Disabled">
|
||||
<cwc:SettingsExpander
|
||||
Description="{Binding UserOptions.UserName}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<Button
|
||||
Command="{Binding Passport.LoginCommand}"
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLoginAction}"
|
||||
Style="{ThemeResource SettingButtonStyle}"
|
||||
Visibility="{Binding UserOptions.IsLoggedIn, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
<Button
|
||||
Command="{Binding Passport.RegisterCommand}"
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingHutaoPassportRegisterAction}"
|
||||
Style="{ThemeResource SettingButtonStyle}"
|
||||
Visibility="{Binding UserOptions.IsLoggedIn, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
<Button
|
||||
Command="{Binding Passport.LogoutCommand}"
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLogoutAction}"
|
||||
Style="{ThemeResource SettingButtonStyle}"
|
||||
Visibility="{Binding UserOptions.IsLoggedIn, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
<cwc:SettingsExpander.Items>
|
||||
<cwc:SettingsCard
|
||||
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
|
||||
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperHeader}"
|
||||
Visibility="{Binding UserOptions.IsLicensedDeveloper, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<cwc:SettingsCard
|
||||
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
|
||||
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerHeader}"
|
||||
Visibility="{Binding UserOptions.IsMaintainer, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<cwc:SettingsCard Description="{Binding UserOptions.GachaLogExpireAtSlim}" Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportGachaLogExpiredAtHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
Command="{Binding Passport.OpenRedeemWebsiteCommand}"
|
||||
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportRedeemCodeDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportRedeemCodeHeader}"
|
||||
IsClickEnabled="True"/>
|
||||
<cwc:SettingsCard
|
||||
Background="{ThemeResource SystemFillColorCriticalBackgroundBrush}"
|
||||
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportDangerZoneDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportDangerZoneHeader}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<Button
|
||||
Command="{Binding Passport.ResetPasswordCommand}"
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingHutaoPassportResetPasswordAction}"
|
||||
Style="{ThemeResource SettingButtonStyle}"/>
|
||||
<Button
|
||||
Command="{Binding Passport.UnregisterCommand}"
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingHutaoPassportUnregisterAction}"
|
||||
Style="{ThemeResource SettingButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
</cwc:SettingsExpander.Items>
|
||||
</cwc:SettingsExpander>
|
||||
</ScrollViewer>
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingGeetestVerificationHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
ActionIcon="{shcm:FontIcon Glyph=}"
|
||||
|
||||
@@ -104,7 +104,8 @@
|
||||
<Expander
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Upload Announcement">
|
||||
Header="Upload Announcement"
|
||||
IsExpanded="True">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBox
|
||||
Header="Title"
|
||||
@@ -114,6 +115,10 @@
|
||||
Header="Link"
|
||||
PlaceholderText="Please input link"
|
||||
Text="{Binding Announcement.Link, Mode=TwoWay}"/>
|
||||
<TextBox
|
||||
Header="Link"
|
||||
PlaceholderText="Max present version(leave empty to present in any version)"
|
||||
Text="{Binding Announcement.MaxPresentVersion, Mode=TwoWay}"/>
|
||||
<TextBox
|
||||
AcceptsReturn="True"
|
||||
Header="Content"
|
||||
|
||||
@@ -199,7 +199,10 @@
|
||||
BorderThickness="0"
|
||||
DefaultLabelPosition="Right">
|
||||
<CommandBar.Content>
|
||||
<shcp:PanelSelector x:Name="ItemsPanelSelector" Margin="8,8,0,0"/>
|
||||
<shcp:PanelSelector
|
||||
x:Name="ItemsPanelSelector"
|
||||
Margin="8,8,0,0"
|
||||
LocalSettingKeySuffixForCurrent="WikiAvatarPage.Avatars"/>
|
||||
</CommandBar.Content>
|
||||
<!--<AppBarButton Icon="{shcm:FontIcon Glyph=}" Label="搜索提示"/>-->
|
||||
<AppBarElementContainer>
|
||||
|
||||
@@ -76,7 +76,10 @@
|
||||
BorderThickness="0"
|
||||
DefaultLabelPosition="Right">
|
||||
<CommandBar.Content>
|
||||
<shcp:PanelSelector x:Name="ItemsPanelSelector" Margin="6,8,0,0"/>
|
||||
<shcp:PanelSelector
|
||||
x:Name="ItemsPanelSelector"
|
||||
Margin="6,8,0,0"
|
||||
LocalSettingKeySuffixForCurrent="WikiMonsterPage.Monsters"/>
|
||||
</CommandBar.Content>
|
||||
<AppBarElementContainer Visibility="Collapsed">
|
||||
<AutoSuggestBox
|
||||
|
||||
@@ -97,7 +97,10 @@
|
||||
BorderThickness="0"
|
||||
DefaultLabelPosition="Right">
|
||||
<CommandBar.Content>
|
||||
<shcp:PanelSelector x:Name="ItemsPanelSelector" Margin="6,8,0,0"/>
|
||||
<shcp:PanelSelector
|
||||
x:Name="ItemsPanelSelector"
|
||||
Margin="6,8,0,0"
|
||||
LocalSettingKeySuffixForCurrent="WikiWeaponPage.Weapons"/>
|
||||
</CommandBar.Content>
|
||||
<AppBarElementContainer>
|
||||
<AutoSuggestBox
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Glyph=""/>
|
||||
Glyph="{StaticResource FontIconContentChevronRight}"/>
|
||||
</Grid>
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
@@ -144,14 +144,14 @@
|
||||
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
|
||||
Margin="2,-4"
|
||||
Command="{Binding LoginMihoyoUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentWebsite}}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
|
||||
<AppBarButton
|
||||
Width="{StaticResource LargeAppBarButtonWidth}"
|
||||
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
|
||||
Margin="2,-4"
|
||||
Command="{Binding AddUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentAdd}}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
|
||||
<MenuFlyoutSeparator Margin="4,0"/>
|
||||
<TextBlock
|
||||
@@ -164,14 +164,14 @@
|
||||
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
|
||||
Margin="2,-4"
|
||||
Command="{Binding LoginHoyoverseUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentWebsite}}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
|
||||
<AppBarButton
|
||||
Width="{StaticResource LargeAppBarButtonWidth}"
|
||||
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
|
||||
Margin="2,-4"
|
||||
Command="{Binding AddOverseaUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentAdd}}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
|
||||
<MenuFlyoutSeparator Margin="4,0"/>
|
||||
<TextBlock
|
||||
@@ -184,7 +184,7 @@
|
||||
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
|
||||
Margin="2,-4"
|
||||
AllowFocusOnInteraction="True"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentHomeGroup}}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationGameRecordIndexAction}"
|
||||
Style="{StaticResource DefaultAppBarButtonStyle}">
|
||||
<AppBarButton.Flyout>
|
||||
@@ -211,7 +211,7 @@
|
||||
AllowFocusOnInteraction="True"
|
||||
Command="{Binding ClaimSignInRewardCommand}"
|
||||
CommandParameter="{Binding ElementName=SignInRewardButton}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentGiftboxOpen}}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationSignInRewardAction}"
|
||||
Style="{StaticResource DefaultAppBarButtonStyle}">
|
||||
<FlyoutBase.AttachedFlyout>
|
||||
@@ -240,7 +240,7 @@
|
||||
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
|
||||
Margin="2,-4"
|
||||
Command="{Binding RefreshCookieTokenCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentRefresh}}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationRefreshCookieAction}"/>
|
||||
</StackPanel>
|
||||
|
||||
@@ -314,7 +314,7 @@
|
||||
Background="Transparent"
|
||||
Command="{Binding DataContext.CopyCookieCommand, Source={StaticResource ViewModelBindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content=""
|
||||
Content="{StaticResource FontIconContentCopy}"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
Style="{StaticResource ButtonRevealStyle}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewUserCopyCookieAction}"/>
|
||||
@@ -325,7 +325,7 @@
|
||||
Background="Transparent"
|
||||
Command="{Binding DataContext.RemoveUserCommand, Source={StaticResource ViewModelBindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content=""
|
||||
Content="{StaticResource FontIconContentDelete}"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
Style="{StaticResource ButtonRevealStyle}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewUserRemoveAction}"/>
|
||||
|
||||
@@ -5,7 +5,8 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.DataTransfer;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Factory.Picker;
|
||||
using Snap.Hutao.Model.InterChange.Achievement;
|
||||
using Snap.Hutao.Service.Achievement;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
|
||||
@@ -5,7 +5,8 @@ using CommunityToolkit.WinUI.Collections;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Factory.Picker;
|
||||
using Snap.Hutao.Model.InterChange.Achievement;
|
||||
using Snap.Hutao.Service.Achievement;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
|
||||
@@ -8,7 +8,7 @@ using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Control.Media;
|
||||
using Snap.Hutao.Core.IO.DataTransfer;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Message;
|
||||
using Snap.Hutao.Model.Calculable;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
|
||||
@@ -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.Model.Entity;
|
||||
using Snap.Hutao.Model.Metadata.Item;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.DailyNote;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
|
||||
@@ -6,7 +6,9 @@ using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Factory.Picker;
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.InterChange.GachaLog;
|
||||
using Snap.Hutao.Service.GachaLog;
|
||||
@@ -27,10 +29,11 @@ namespace Snap.Hutao.ViewModel.GachaLog;
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly HutaoCloudStatisticsViewModel hutaoCloudStatisticsViewModel;
|
||||
private readonly IGachaLogQueryProviderFactory gachaLogQueryProviderFactory;
|
||||
private readonly IContentDialogFactory contentDialogFactory;
|
||||
private readonly HutaoCloudStatisticsViewModel hutaoCloudStatisticsViewModel;
|
||||
private readonly HutaoCloudViewModel hutaoCloudViewModel;
|
||||
private readonly IProgressFactory progressFactory;
|
||||
private readonly IGachaLogService gachaLogService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly JsonSerializerOptions options;
|
||||
@@ -162,7 +165,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
|
||||
throw;
|
||||
}
|
||||
|
||||
IProgress<GachaLogFetchStatus> progress = taskContext.CreateProgressForMainThread<GachaLogFetchStatus>(dialog.OnReport);
|
||||
IProgress<GachaLogFetchStatus> progress = progressFactory.CreateForMainThread<GachaLogFetchStatus>(dialog.OnReport);
|
||||
bool authkeyValid;
|
||||
|
||||
try
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.GachaLog;
|
||||
using Snap.Hutao.Service.Hutao;
|
||||
|
||||
@@ -18,6 +18,11 @@ internal sealed partial class TypedWishSummary : Wish
|
||||
private double probabilityOfPredictedPullLeftToOrange;
|
||||
private double probabilityOfNextPullIsOrange;
|
||||
|
||||
/// <summary>
|
||||
/// 类型名称,不受语言影响
|
||||
/// </summary>
|
||||
public string? TypeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大五星抽数
|
||||
/// </summary>
|
||||
|
||||
@@ -5,11 +5,14 @@ using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Service.Game.Configuration;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Snap.Hutao.Service.User;
|
||||
@@ -36,13 +39,14 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
private readonly IContentDialogFactory contentDialogFactory;
|
||||
private readonly LaunchStatusOptions launchStatusOptions;
|
||||
private readonly INavigationService navigationService;
|
||||
private readonly IProgressFactory progressFactory;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly ResourceClient resourceClient;
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
private readonly LaunchOptions launchOptions;
|
||||
private readonly RuntimeOptions hutaoOptions;
|
||||
private readonly IUserService userService;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly IGameService gameService;
|
||||
private readonly IGameServiceFacade gameService;
|
||||
private readonly IMemoryCache memoryCache;
|
||||
private readonly AppOptions appOptions;
|
||||
|
||||
@@ -94,7 +98,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
/// <summary>
|
||||
/// 胡桃选项
|
||||
/// </summary>
|
||||
public RuntimeOptions HutaoOptions { get => hutaoOptions; }
|
||||
public RuntimeOptions HutaoOptions { get => runtimeOptions; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用选项
|
||||
@@ -189,45 +193,44 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
[Command("LaunchCommand")]
|
||||
private async Task LaunchAsync()
|
||||
{
|
||||
if (SelectedScheme is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
gameService.SetChannelOptions(SelectedScheme);
|
||||
|
||||
// Whether or not the channel options changed, we always ensure game resouces
|
||||
LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
|
||||
IProgress<PackageReplaceStatus> convertProgress = taskContext.CreateProgressForMainThread<PackageReplaceStatus>(state => dialog.State = state);
|
||||
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
|
||||
{
|
||||
if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false))
|
||||
{
|
||||
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (SelectedGameAccount is not null)
|
||||
{
|
||||
if (!gameService.SetGameAccount(SelectedGameAccount))
|
||||
{
|
||||
infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
IProgress<LaunchStatus> launchProgress = taskContext.CreateProgressForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status);
|
||||
await gameService.LaunchAsync(launchProgress).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(ExceptionFormat.Format(ex));
|
||||
infoBarService.Error(ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
if (SelectedScheme is null)
|
||||
{
|
||||
infoBarService.Error(SH.ViewModelLaunchGameSchemeNotSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Always ensure game resources
|
||||
gameService.SetChannelOptions(SelectedScheme);
|
||||
|
||||
LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
|
||||
IProgress<PackageReplaceStatus> convertProgress = progressFactory.CreateForMainThread<PackageReplaceStatus>(state => dialog.State = state);
|
||||
|
||||
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
|
||||
{
|
||||
if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false))
|
||||
{
|
||||
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (SelectedGameAccount is not null)
|
||||
{
|
||||
if (!gameService.SetGameAccount(SelectedGameAccount))
|
||||
{
|
||||
infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
IProgress<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status);
|
||||
await gameService.LaunchAsync(launchProgress).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
infoBarService.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user