Compare commits

..

53 Commits

Author SHA1 Message Date
DismissedLight
b0ecd048b6 1.7.16 package 2023-11-06 21:41:04 +08:00
DismissedLight
91010d0d8b Merge pull request #1076 from DGP-Studio/develop 2023-11-06 20:57:23 +08:00
Masterain
fc771eb90a New Crowdin updates (#1063) 2023-11-06 20:56:40 +08:00
DismissedLight
80f2fed722 Merge pull request #1075 from DGP-Studio/develop 2023-11-06 20:56:15 +08:00
DismissedLight
bdb406c451 add copy hint for #1074 2023-11-06 20:53:29 +08:00
DismissedLight
5bc957c6a5 fix spiral abyss crash when using 4.2 metadata 2023-11-06 20:32:29 +08:00
Lightczx
416c6f15a6 partial #1074 2023-11-06 17:07:35 +08:00
Lightczx
9eed633e05 DefaultItemCollectionTransitionProvider 2023-11-06 15:52:50 +08:00
Lightczx
7e30173990 update to 4.2 metadata 2023-11-06 14:57:57 +08:00
Lightczx
2200e2e58e fonticon resources 2023-11-06 14:36:36 +08:00
Lightczx
b8886c5cd3 fix #1072 2023-11-06 13:46:59 +08:00
DismissedLight
43007d8fb4 Merge pull request #1073 from qhy040404/develop 2023-11-06 12:40:53 +08:00
Lightczx
88684bff00 code style 2023-11-06 12:40:19 +08:00
qhy040404
0c7ce7a72f Add files 2023-11-06 12:32:22 +08:00
qhy040404
075d92f754 Set IsEnabled by a new property instead of setting it separately for each SettingsCard 2023-11-06 12:18:05 +08:00
Lightczx
a0cba171cc update feat template 2023-11-06 11:43:57 +08:00
Lightczx
f41185310b adjust wish typename 2023-11-06 11:41:33 +08:00
Lightczx
2a4c93d241 impl #778 all 2023-11-06 11:17:16 +08:00
DismissedLight
c0980fabe8 impl #1071 2023-11-05 16:03:10 +08:00
DismissedLight
f2ba316059 recycle fingerprint 2023-11-04 17:47:45 +08:00
DismissedLight
e4e9dd91f1 impl #1062 2023-11-04 17:21:31 +08:00
DismissedLight
749ef0e138 introducing game service facade 2023-11-04 16:53:08 +08:00
DismissedLight
24086ee4d0 optimize UniformStaggeredColumnLayout 2023-11-03 23:32:52 +08:00
DismissedLight
aeb6962ae4 impl #1015 2023-11-03 22:06:51 +08:00
Lightczx
87e5ede91f impl #1068 2023-11-03 16:20:11 +08:00
Lightczx
91de6d170e add fingerprint fetch & fix #1060 2023-11-03 11:52:52 +08:00
Lightczx
3057673cdb fix #1069 2023-11-03 10:11:47 +08:00
Lightczx
c3ace405ac fix pushpage 2023-11-03 09:26:44 +08:00
DismissedLight
0b48581e65 Merge pull request #1065 from qhy040404/main 2023-11-02 21:16:40 +08:00
DismissedLight
4ab129e4a2 code style 2023-11-02 21:14:39 +08:00
qhy040404
13ad36f5b4 Added a master switch for launchOptions 2023-11-02 18:43:07 +08:00
Lightczx
f026321aa8 1.7.15 package 2023-11-02 15:27:40 +08:00
DismissedLight
d1dfdf107b Merge pull request #1064 from DGP-Studio/develop 2023-11-02 15:10:51 +08:00
Lightczx
59f8895675 MarqueeText 2023-11-02 15:05:06 +08:00
Lightczx
4cb3d5f03f launch scheme renewed 2023-11-02 14:23:34 +08:00
Lightczx
067c7d7c4d fix ci 2023-11-02 12:45:11 +08:00
Lightczx
1cc072ba28 EmailSmtpAddress 2023-11-02 11:35:31 +08:00
Lightczx
0e7afa8efb clear username & password after cancel registration 2023-11-02 11:30:00 +08:00
Lightczx
b753728b7e verify code request set token 2023-11-02 11:12:26 +08:00
Lightczx
df019da891 complete cancel registration 2023-11-02 10:42:55 +08:00
Lightczx
c6435f30eb add verify for cancel registration 2023-11-02 10:26:27 +08:00
Lightczx
3ac0be4220 fix unregister passport 2023-11-02 10:07:20 +08:00
Lightczx
24f6a33256 clear username & password after logout 2023-11-02 09:48:22 +08:00
Lightczx
dc9278eb4f fix verifycode crash 2023-11-02 09:42:08 +08:00
Lightczx
4b2c82db62 fix xaml parsing failed 2023-11-02 09:06:56 +08:00
Lightczx
70f30edd7c xaml style rework 2023-11-01 17:03:00 +08:00
Lightczx
c8e8213df6 code style 2023-11-01 15:56:22 +08:00
Lightczx
7cad996902 process cmdline #1061 2023-11-01 15:28:44 +08:00
Lightczx
eec47b72c7 fix #1061 2023-11-01 15:26:36 +08:00
Lightczx
5943b1a1fb impl #886 2023-11-01 13:46:50 +08:00
Lightczx
9f9a5670bc fix dailynote webhook error 2023-11-01 10:45:47 +08:00
Lightczx
10ba927136 fix #1059 2023-10-31 16:45:24 +08:00
Lightczx
07d42cedd1 page style 2023-10-31 15:19:25 +08:00
158 changed files with 5186 additions and 3255 deletions

View File

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

View File

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

View File

@@ -12,6 +12,7 @@
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/FontStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Glyph.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/InfoBarOverride.xaml"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -42,8 +42,8 @@ internal sealed class TaskContext : ITaskContext
dispatcherQueue.Invoke(action);
}
public IProgress<T> CreateProgressForMainThread<T>(Action<T> handler)
public SynchronizationContext GetSynchronizationContext()
{
return new DispatcherQueueProgress<T>(handler, synchronizationContext);
return synchronizationContext;
}
}

View File

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

View File

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

View File

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

View File

@@ -4,11 +4,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;
}

View File

@@ -0,0 +1,6 @@
namespace Snap.Hutao.Factory.Progress;
internal interface IProgressFactory
{
IProgress<T> CreateForMainThread<T>(Action<T> handler);
}

View File

@@ -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());
}
}

View File

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

View File

@@ -0,0 +1,29 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
/// <inheritdoc />
public partial class AddUserFingerprint : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Fingerprint",
table: "users",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Fingerprint",
table: "users");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -539,9 +539,15 @@
<data name="ServerPassportVerifyFailed" xml:space="preserve">
<value>Verification failed</value>
</data>
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
<value>The verification request failed, it is not the currently logged in account</value>
</data>
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
<value>The verification code has been sent to your e-mail.</value>
</data>
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
<value>The verification request failed, the current email address has been registered</value>
</data>
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
<value>Validation request is too frequent. Please try again in 1 minute.</value>
</data>
@@ -1796,6 +1802,9 @@
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
<value>Input</value>
</data>
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
<value>Recover Wish Record from Snap Hutao Cloud</value>
</data>
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
<value>Refresh</value>
</data>
@@ -1958,6 +1967,12 @@
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
<value>Advanced Features</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
<value>Resolution Ratio Shortcut</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>Screen Resolution</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>Create window as popup, without frame</value>
</data>
@@ -2006,6 +2021,9 @@
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>File</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>InterProcess</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>Run the software on the selected display</value>
</data>
@@ -2021,6 +2039,12 @@
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
<value>Game Options</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>Try to start the game after the game is started and use Starward for game duration statistics</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>Hours Played</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>Progress</value>
</data>
@@ -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>

View File

@@ -539,9 +539,15 @@
<data name="ServerPassportVerifyFailed" xml:space="preserve">
<value>認証に失敗しました。</value>
</data>
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
<value>認証リクエストに失敗しました。現在ログインしているアカウントではありません。</value>
</data>
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
<value>入力されたメールアドレスへ確認コードが送信されました</value>
</data>
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
<value>認証リクエストに失敗しました。このメールアドレスは既に登録されています。</value>
</data>
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
<value>認証リクエストが多すぎます。一分後にやり直してください。</value>
</data>
@@ -1796,6 +1802,9 @@
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
<value>入力</value>
</data>
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
<value>胡桃クラウドから祈願履歴を復元する</value>
</data>
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
<value>情報更新</value>
</data>
@@ -1958,6 +1967,12 @@
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
<value>上級者向け設定</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
<value>指定した解像度に素早く切り替えます</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>解像度</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>枠の無いポップアップウィンドウとして作成します。</value>
</data>
@@ -2006,6 +2021,9 @@
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>ファイル</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程间</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>指定したディスプレイで実行</value>
</data>
@@ -2021,6 +2039,12 @@
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
<value>ゲームオプション</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>时长统计</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>プロセス</value>
</data>
@@ -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>

View File

@@ -539,9 +539,15 @@
<data name="ServerPassportVerifyFailed" xml:space="preserve">
<value>验证失败</value>
</data>
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
<value>验证请求失败,不是当前登录的账号</value>
</data>
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
<value>验证码已发送至邮箱</value>
</data>
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
<value>验证请求失败,当前邮箱已被注册</value>
</data>
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
<value>验证请求过快,请 1 分钟后再试</value>
</data>
@@ -1796,6 +1802,9 @@
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
<value>입력</value>
</data>
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
<value>从胡桃云恢复祈愿记录</value>
</data>
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
<value>동기화</value>
</data>
@@ -1958,6 +1967,12 @@
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
<value>고급</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
<value>快速切换到指定的分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>테두리 없는 창모드</value>
</data>
@@ -2006,6 +2021,9 @@
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>文件</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程间</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>지정한 모니터에서 실행</value>
</data>
@@ -2021,6 +2039,12 @@
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
<value>게임 설정</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>时长统计</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>进程</value>
</data>
@@ -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>

View File

@@ -539,9 +539,15 @@
<data name="ServerPassportVerifyFailed" xml:space="preserve">
<value>验证失败</value>
</data>
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
<value>验证请求失败,不是当前登录的账号</value>
</data>
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
<value>验证码已发送至邮箱</value>
</data>
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
<value>验证请求失败,当前邮箱已被注册</value>
</data>
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
<value>验证请求过快,请 1 分钟后再试</value>
</data>
@@ -1796,6 +1802,9 @@
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
<value>输入</value>
</data>
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
<value>从胡桃云恢复祈愿记录</value>
</data>
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
<value>刷新</value>
</data>
@@ -1958,6 +1967,12 @@
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
<value>高级功能</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
<value>快速切换到指定的分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>将窗口创建为弹出窗口,不带框架</value>
</data>
@@ -2006,6 +2021,9 @@
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>文件</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程间</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>在指定的显示器上运行</value>
</data>
@@ -2021,6 +2039,12 @@
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
<value>游戏选项</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>时长统计</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>进程</value>
</data>
@@ -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>
@@ -2714,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>

View File

@@ -539,9 +539,15 @@
<data name="ServerPassportVerifyFailed" xml:space="preserve">
<value>验证失败</value>
</data>
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
<value>验证请求失败,不是当前登录的账号</value>
</data>
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
<value>验证码已发送至邮箱</value>
</data>
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
<value>验证请求失败,当前邮箱已被注册</value>
</data>
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
<value>验证请求过快,请 1 分钟后再试</value>
</data>
@@ -1689,7 +1695,7 @@
<value>新增</value>
</data>
<data name="ViewPageDailyNoteAttendanceStatusInfo" xml:space="preserve">
<value>历练点获取详情</value>
<value>歷練點獲取詳情</value>
</data>
<data name="ViewPageDailyNoteConfigWebhookDescription" xml:space="preserve">
<value>在实时便笺刷新后推送到指定的 Webhook</value>
@@ -1796,6 +1802,9 @@
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
<value>輸入</value>
</data>
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
<value>從胡桃云恢復祈願紀錄</value>
</data>
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
<value>重新整理</value>
</data>
@@ -1958,6 +1967,12 @@
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
<value>進階功能</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
<value>快速切换到指定的分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
<value>分辨率</value>
</data>
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
<value>將窗口創建為彈出窗口,不帶邊框</value>
</data>
@@ -2006,6 +2021,9 @@
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
<value>文件</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程间</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>在指定的屏幕上運行</value>
</data>
@@ -2021,6 +2039,12 @@
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
<value>遊戲選項</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>时长统计</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>进程</value>
</data>
@@ -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>

View File

@@ -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
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
[Injection(InjectAs.Transient)]
internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider
{
private readonly IGameService gameService;
private readonly IGameServiceFacade gameService;
private readonly MetadataOptions metadataOptions;
/// <summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Options;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Web.Hutao;
using System.Text.RegularExpressions;
@@ -62,8 +63,11 @@ internal sealed class HutaoUserOptions : ObservableObject, IOptions<HutaoUserOpt
/// <inheritdoc/>
public HutaoUserOptions Value { get => this; }
public async ValueTask<bool> PostLoginSucceedAsync(HomaPassportClient passportClient, ITaskContext taskContext, string username, string? token)
public async ValueTask<bool> PostLoginSucceedAsync(HomaPassportClient passportClient, ITaskContext taskContext, string username, string password, string? token)
{
LocalSetting.Set(SettingKeys.PassportUserName, username);
LocalSetting.Set(SettingKeys.PassportPassword, password);
await taskContext.SwitchToMainThreadAsync();
UserName = username;
this.token = token;
@@ -84,6 +88,9 @@ internal sealed class HutaoUserOptions : ObservableObject, IOptions<HutaoUserOpt
public void LogoutOrUnregister()
{
LocalSetting.Set(SettingKeys.PassportUserName, string.Empty);
LocalSetting.Set(SettingKeys.PassportPassword, string.Empty);
UserName = null;
token = null;
IsLoggedIn = false;

View File

@@ -32,19 +32,19 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe
public async ValueTask InitializeInternalAsync(CancellationToken token = default)
{
string userName = LocalSetting.Get(SettingKeys.PassportUserName, string.Empty);
string passport = LocalSetting.Get(SettingKeys.PassportPassword, string.Empty);
string password = LocalSetting.Get(SettingKeys.PassportPassword, string.Empty);
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(passport))
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
{
options.SkipLogin();
}
else
{
Web.Response.Response<string> response = await passportClient.LoginAsync(userName, passport, token).ConfigureAwait(false);
Web.Response.Response<string> response = await passportClient.LoginAsync(userName, password, token).ConfigureAwait(false);
if (response.IsOk())
{
if (await options.PostLoginSucceedAsync(passportClient, taskContext, userName, response.Data).ConfigureAwait(false))
if (await options.PostLoginSucceedAsync(passportClient, taskContext, userName, password, response.Data).ConfigureAwait(false))
{
isInitialized = true;
}

View File

@@ -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);
}

View File

@@ -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));
}

View 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();
}
}

View File

@@ -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;
}

View File

@@ -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)
{

View File

@@ -79,6 +79,7 @@
<None Remove="Control\Theme\Color.xaml" />
<None Remove="Control\Theme\Converter.xaml" />
<None Remove="Control\Theme\CornerRadius.xaml" />
<None Remove="Control\Theme\FlyoutStyle.xaml" />
<None Remove="Control\Theme\FontStyle.xaml" />
<None Remove="Control\Theme\Glyph.xaml" />
<None Remove="Control\Theme\InfoBarOverride.xaml" />
@@ -276,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>
@@ -307,6 +308,12 @@
<AdditionalFiles Include="Resource\Localization\SH.resx" />
</ItemGroup>
<ItemGroup>
<Page Update="Control\Theme\FlyoutStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\HutaoPassportUnregisterDialog.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -42,7 +42,7 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="{ThemeResource TitleTextBlockFontSize}"
Glyph="&#xE7FC;"/>
Glyph="{StaticResource FontIconContentGame}"/>
<TextBlock
Grid.Row="1"
Grid.Column="0"

View File

@@ -10,7 +10,7 @@
Background="{ThemeResource SystemFillColorCriticalBackgroundBrush}"
Content="{shcm:ResourceString Name=ViewControlElevationText}"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Icon="{shcm:FontIcon Glyph=&#xE7BA;}"
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentWarning}}"
mc:Ignorable="d">
<clw:TokenItem.Resources>
<x:Double x:Key="ListViewItemDisabledThemeOpacity">1</x:Double>

View File

@@ -48,9 +48,9 @@
Height="38.4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="&#xE8A7;"
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="&#xE8A7;"
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"

View File

@@ -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>

View File

@@ -13,7 +13,11 @@
mc:Ignorable="d">
<StackPanel Margin="0,8,0,0">
<TextBox PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportUserNameHint}" Text="{x:Bind UserName, Mode=TwoWay}"/>
<TextBox
InputScope="EmailSmtpAddress"
IsSpellCheckEnabled="False"
PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportUserNameHint}"
Text="{x:Bind UserName, Mode=TwoWay}"/>
<PasswordBox
Margin="0,16,0,0"
Password="{x:Bind Password, Mode=TwoWay}"

View File

@@ -13,7 +13,11 @@
mc:Ignorable="d">
<StackPanel Margin="0,8,0,0">
<TextBox PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportUserNameHint}" Text="{x:Bind UserName, Mode=TwoWay}"/>
<TextBox
InputScope="EmailSmtpAddress"
IsSpellCheckEnabled="False"
PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportUserNameHint}"
Text="{x:Bind UserName, Mode=TwoWay}"/>
<Grid Margin="0,16,0,0" ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
@@ -28,7 +32,7 @@
</Grid>
<PasswordBox
Margin="0,16,0,0"
IsEnabled="{x:Bind VerifyCode, Converter={StaticResource StringBoolConverter}}"
IsEnabled="{x:Bind VerifyCode, Converter={StaticResource StringBoolConverter}, Mode=OneWay}"
Password="{x:Bind Password, Mode=TwoWay}"
PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportPasswordHint}"/>
<TextBlock
@@ -37,4 +41,4 @@
Style="{StaticResource CaptionTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageHutaoPassportPasswordRequirementHint}"/>
</StackPanel>
</ContentDialog>
</ContentDialog>

View File

@@ -48,7 +48,7 @@ internal sealed partial class HutaoPassportRegisterDialog : ContentDialog
return;
}
HutaoResponse response = await homaPassportClient.VerifyAsync(UserName, false).ConfigureAwait(false);
HutaoResponse response = await homaPassportClient.RequestVerifyAsync(UserName, VerifyCodeRequestType.Registration).ConfigureAwait(false);
infoBarService.Information(response.GetLocalizationMessage());
}
}

View File

@@ -13,7 +13,11 @@
mc:Ignorable="d">
<StackPanel>
<TextBox PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportUserNameHint}" Text="{x:Bind UserName, Mode=TwoWay}"/>
<TextBox
InputScope="EmailSmtpAddress"
IsSpellCheckEnabled="False"
PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportUserNameHint}"
Text="{x:Bind UserName, Mode=TwoWay}"/>
<Grid Margin="0,16,0,0" ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
@@ -28,7 +32,7 @@
</Grid>
<PasswordBox
Margin="0,16,0,0"
IsEnabled="{x:Bind VerifyCode, Converter={StaticResource StringBoolConverter}}"
IsEnabled="{x:Bind VerifyCode, Converter={StaticResource StringBoolConverter}, Mode=OneWay}"
Password="{x:Bind Password, Mode=TwoWay}"
PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportPasswordHint}"/>
<TextBlock
@@ -37,4 +41,4 @@
Style="{StaticResource CaptionTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageHutaoPassportPasswordRequirementHint}"/>
</StackPanel>
</ContentDialog>
</ContentDialog>

View File

@@ -48,7 +48,7 @@ internal sealed partial class HutaoPassportResetPasswordDialog : ContentDialog
return;
}
HutaoResponse response = await homaPassportClient.VerifyAsync(UserName, false).ConfigureAwait(false);
HutaoResponse response = await homaPassportClient.RequestVerifyAsync(UserName, VerifyCodeRequestType.ResetPassword).ConfigureAwait(false);
infoBarService.Information(response.GetLocalizationMessage());
}
}

View File

@@ -20,11 +20,26 @@
Severity="Error"/>
<TextBox
Margin="0,16,0,0"
InputScope="EmailSmtpAddress"
IsSpellCheckEnabled="False"
PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportUserNameHint}"
Text="{x:Bind UserName, Mode=TwoWay}"/>
<Grid Margin="0,16,0,0" ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBox PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportVerifyCodeHint}" Text="{x:Bind VerifyCode, Mode=TwoWay}"/>
<Button
Grid.Column="1"
VerticalAlignment="Stretch"
Command="{x:Bind VerifyCommand}"
Content="{shcm:ResourceString Name=ViewPageHutaoPassportVerifyCodeAction}"/>
</Grid>
<PasswordBox
Margin="0,16,0,0"
IsEnabled="{x:Bind VerifyCode, Converter={StaticResource StringBoolConverter}, Mode=OneWay}"
Password="{x:Bind Password, Mode=TwoWay}"
PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportPasswordHint}"/>
</StackPanel>
</ContentDialog>
</ContentDialog>

View File

@@ -1,14 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Common;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Web.Hutao;
namespace Snap.Hutao.View.Dialog;
[DependencyProperty("UserName", typeof(string))]
[DependencyProperty("Password", typeof(string))]
[DependencyProperty("VerifyCode", typeof(string))]
internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog
{
private readonly HomaPassportClient homaPassportClient;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
public HutaoPassportUnregisterDialog(IServiceProvider serviceProvider)
@@ -16,13 +22,33 @@ internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog
InitializeComponent();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
homaPassportClient = serviceProvider.GetRequiredService<HomaPassportClient>();
infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
}
public async ValueTask<ValueResult<bool, (string UserName, string Passport)>> GetInputAsync()
public async ValueTask<ValueResult<bool, (string UserName, string Passport, string VerifyCode)>> GetInputAsync()
{
await taskContext.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();
return new(result is ContentDialogResult.Primary, (UserName, Password));
return new(result is ContentDialogResult.Primary, (UserName, Password, VerifyCode));
}
[Command("VerifyCommand")]
private async Task VerifyAsync()
{
if (string.IsNullOrEmpty(UserName))
{
return;
}
if (!UserName.IsEmail())
{
infoBarService.Warning(SH.ViewModelHutaoPassportEmailNotValidHint);
return;
}
HutaoResponse response = await homaPassportClient.RequestVerifyAsync(UserName, VerifyCodeRequestType.CancelRegistration).ConfigureAwait(false);
infoBarService.Information(response.GetLocalizationMessage());
}
}

View File

@@ -12,11 +12,9 @@
MaxWidth="640"
Margin="32,48,32,32"
VerticalAlignment="Bottom"
ItemContainerTransitions="{StaticResource RepositionThemeTransitions}"
ItemsSource="{x:Bind InfoBars}"
Visibility="{Binding ElementName=VisibilityButton, Path=IsChecked, Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl.Transitions>
<RepositionThemeTransition/>
</ItemsControl.Transitions>
Visibility="{x:Bind VisibilityButton.IsChecked, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<ItemsControl.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>

View File

@@ -17,9 +17,9 @@
<NavigationView
x:Name="NavView"
CompactPaneLength="48"
IsBackEnabled="{Binding ElementName=ContentFrame, Path=CanGoBack}"
IsBackEnabled="{x:Bind ContentFrame.CanGoBack, Mode=OneWay}"
IsPaneOpen="True"
OpenPaneLength="188"
OpenPaneLength="192"
PaneDisplayMode="Left"
UseLayoutRounding="False">
<NavigationView.MenuItems>
@@ -86,13 +86,10 @@
<shv:UserView/>
</NavigationView.PaneFooter>
<Frame x:Name="ContentFrame" UseLayoutRounding="True">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition/>
</TransitionCollection>
</Frame.ContentTransitions>
</Frame>
<Frame
x:Name="ContentFrame"
ContentTransitions="{StaticResource NavigationThemeTransitions}"
UseLayoutRounding="True"/>
</NavigationView>
<shv:InfoBarView/>

View File

@@ -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
@@ -164,6 +165,21 @@
</Grid>
</Border>
</DataTemplate>
<Style
x:Key="AchievementGoalListViewItemStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}"
TargetType="ListViewItem">
<Setter Property="Margin" Value="0,0,6,0"/>
</Style>
<Style
x:Key="AchievementListViewItemStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}"
TargetType="ListViewItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0,4,8,0"/>
</Style>
</Page.Resources>
<mxi:Interaction.Behaviors>
@@ -186,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"
@@ -282,6 +301,7 @@
PaneBackground="{x:Null}">
<SplitView.Pane>
<ListView
ItemContainerStyle="{StaticResource AchievementGoalListViewItemStyle}"
ItemTemplate="{StaticResource AchievementGoalListTemplate}"
ItemsSource="{Binding AchievementGoals}"
SelectedItem="{Binding SelectedAchievementGoal, Mode=TwoWay}"
@@ -289,11 +309,6 @@
<mxi:Interaction.Behaviors>
<shcb:SelectedItemInViewBehavior/>
</mxi:Interaction.Behaviors>
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="Margin" Value="0,0,6,0"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
</SplitView.Pane>
@@ -301,16 +316,10 @@
<ListView
Margin="8,0,0,0"
Padding="0,0,0,8"
ItemContainerStyle="{StaticResource AchievementListViewItemStyle}"
ItemTemplate="{StaticResource AchievementTemplate}"
ItemsSource="{Binding Achievements}"
SelectionMode="None">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0,4,8,0"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
SelectionMode="None"/>
</SplitView.Content>
</SplitView>
</cwc:Case>

View File

@@ -29,9 +29,126 @@
<shc:BindingProxy x:Key="BindingProxy" DataContext="{Binding}"/>
<DataTemplate x:Key="AnnouncementTemplate">
<ItemContainer cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Style="{StaticResource BorderCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<!-- Image Layer -->
<Border cw:UIElementExtensions.ClipToBounds="True" CornerRadius="{ThemeResource ControlCornerRadiusTop}">
<Border
x:Name="ImageZoomBorder"
VerticalAlignment="Top"
cw:VisualExtensions.NormalizedCenterPoint="0.5">
<cww:ConstrainedBox AspectRatio="1080:390" CornerRadius="{ThemeResource ControlCornerRadiusTop}">
<shci:CachedImage Source="{Binding Banner}" Stretch="UniformToFill"/>
</cww:ConstrainedBox>
<cwa:Explicit.Animations>
<cwa:AnimationSet x:Name="ImageZoomInAnimation">
<shca:ImageZoomInAnimation/>
</cwa:AnimationSet>
<cwa:AnimationSet x:Name="ImageZoomOutAnimation">
<shca:ImageZoomOutAnimation/>
</cwa:AnimationSet>
</cwa:Explicit.Animations>
</Border>
</Border>
<!-- Time Description -->
<Grid Grid.Row="0">
<Border
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Visibility="{Binding ShouldShowTimeDescription, Converter={StaticResource BoolToVisibilityConverter}}">
<ProgressBar
MinHeight="2"
VerticalAlignment="Bottom"
Background="Transparent"
CornerRadius="0"
Maximum="1"
Value="{Binding TimePercent, Mode=OneWay}"/>
</Border>
</Grid>
<!-- General Description -->
<Border Grid.Row="1">
<StackPanel Margin="4" VerticalAlignment="Bottom">
<TextBlock
Margin="4,6,0,0"
HorizontalAlignment="Stretch"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding Subtitle}"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap"/>
<TextBlock
Margin="4,6,0,0"
Opacity="0.6"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding Title}"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Margin="4,4,0,4"
FontSize="10"
Opacity="0.4"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding TimeFormatted}"
TextWrapping="NoWrap"/>
<TextBlock
Grid.Column="1"
Margin="4,4,4,4"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
FontSize="10"
Opacity="0.8"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding TimeDescription}"
Visibility="{Binding ShouldShowTimeDescription, Converter={StaticResource BoolToVisibilityConverter}}"/>
</Grid>
</StackPanel>
</Border>
</Grid>
<!-- 公告详情网页视图 -->
<FlyoutBase.AttachedFlyout>
<Flyout LightDismissOverlayMode="On" Placement="Full">
<Flyout.FlyoutPresenterStyle>
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MaxWidth" Value="640"/>
</Style>
</Flyout.FlyoutPresenterStyle>
<shvco:AnnouncementContentViewer Announcement="{Binding}"/>
</Flyout>
</FlyoutBase.AttachedFlyout>
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Tapped">
<shcb:OpenAttachedFlyoutAction/>
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerEntered">
<shcb:StartAnimationActionNoThrow Animation="{Binding ElementName=ImageZoomInAnimation}" TargetObject="{Binding ElementName=ImageZoomBorder}"/>
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerExited">
<shcb:StartAnimationActionNoThrow Animation="{Binding ElementName=ImageZoomOutAnimation}" TargetObject="{Binding ElementName=ImageZoomBorder}"/>
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
</Border>
</ItemContainer>
</DataTemplate>
<DataTemplate x:Key="AnnouncementPivotItemContentTemplate">
<ItemsRepeater
Margin="16,16,16,16"
HorizontalAlignment="Stretch"
ItemTemplate="{StaticResource AnnouncementTemplate}"
ItemsSource="{Binding List}">
<ItemsRepeater.Layout>
<UniformGridLayout
@@ -41,118 +158,6 @@
MinItemWidth="300"
MinRowSpacing="12"/>
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<ItemContainer cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Style="{StaticResource BorderCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<!-- Image Layer -->
<Border cw:UIElementExtensions.ClipToBounds="True" CornerRadius="{ThemeResource ControlCornerRadiusTop}">
<Border VerticalAlignment="Top" cw:VisualExtensions.NormalizedCenterPoint="0.5">
<cww:ConstrainedBox AspectRatio="1080:390" CornerRadius="{ThemeResource ControlCornerRadiusTop}">
<shci:CachedImage Source="{Binding Banner}" Stretch="UniformToFill"/>
</cww:ConstrainedBox>
<cwa:Explicit.Animations>
<cwa:AnimationSet x:Name="ImageZoomInAnimation">
<shca:ImageZoomInAnimation/>
</cwa:AnimationSet>
<cwa:AnimationSet x:Name="ImageZoomOutAnimation">
<shca:ImageZoomOutAnimation/>
</cwa:AnimationSet>
</cwa:Explicit.Animations>
</Border>
</Border>
<!-- Time Description -->
<Grid Grid.Row="0">
<Border
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Visibility="{Binding ShouldShowTimeDescription, Converter={StaticResource BoolToVisibilityConverter}}">
<ProgressBar
MinHeight="2"
VerticalAlignment="Bottom"
Background="Transparent"
CornerRadius="0"
Maximum="1"
Value="{Binding TimePercent, Mode=OneWay}"/>
</Border>
</Grid>
<!-- General Description -->
<Border Grid.Row="1">
<StackPanel Margin="4" VerticalAlignment="Bottom">
<TextBlock
Margin="4,6,0,0"
HorizontalAlignment="Stretch"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding Subtitle}"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap"/>
<TextBlock
Margin="4,6,0,0"
Opacity="0.6"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding Title}"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Margin="4,4,0,4"
FontSize="10"
Opacity="0.4"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding TimeFormatted}"
TextWrapping="NoWrap"/>
<TextBlock
Grid.Column="1"
Margin="4,4,4,4"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
FontSize="10"
Opacity="0.8"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding TimeDescription}"
Visibility="{Binding ShouldShowTimeDescription, Converter={StaticResource BoolToVisibilityConverter}}"/>
</Grid>
</StackPanel>
</Border>
</Grid>
<FlyoutBase.AttachedFlyout>
<Flyout LightDismissOverlayMode="On" Placement="Full">
<Flyout.FlyoutPresenterStyle>
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MaxWidth" Value="640"/>
</Style>
</Flyout.FlyoutPresenterStyle>
<shvco:AnnouncementContentViewer Announcement="{Binding}"/>
</Flyout>
</FlyoutBase.AttachedFlyout>
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Tapped">
<shcb:OpenAttachedFlyoutAction/>
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerEntered">
<cwb:StartAnimationAction Animation="{Binding ElementName=ImageZoomInAnimation}"/>
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerExited">
<cwb:StartAnimationAction Animation="{Binding ElementName=ImageZoomOutAnimation}"/>
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
</Border>
</ItemContainer>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</DataTemplate>
@@ -184,7 +189,6 @@
</ItemContainer.Resources>
</ItemContainer>
</DataTemplate>
</shc:ScopedPage.Resources>
<Grid>
@@ -223,11 +227,11 @@
<Pivot Style="{StaticResource DefaultPivotStyle}">
<PivotItem
Content="{Binding Announcement.List[0]}"
ContentTemplate="{StaticResource AnnouncementTemplate}"
ContentTemplate="{StaticResource AnnouncementPivotItemContentTemplate}"
Header="{shcm:ResourceString Name=ViewPageAnnouncementActivity}"/>
<PivotItem
Content="{Binding Announcement.List[1]}"
ContentTemplate="{StaticResource AnnouncementTemplate}"
ContentTemplate="{StaticResource AnnouncementPivotItemContentTemplate}"
Header="{shcm:ResourceString Name=ViewPageAnnouncementGame}"/>
</Pivot>
</StackPanel>

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