Compare commits

...

13 Commits

Author SHA1 Message Date
qhy040404
a2586b0ef2 fix different theme between window and dialog 2024-03-15 20:32:16 +08:00
DismissedLight
eee84a338e significantly reduce uniformstaggeredlayout resize lag 2024-03-15 20:24:32 +08:00
Lightczx
be30362b52 uniformstaggeredlayout optimization 2024-03-15 17:18:25 +08:00
Lightczx
38f36bbb82 fix gachalog page closing crash 2024-03-15 14:21:38 +08:00
Lightczx
704866b16a update dependency 2024-03-15 13:43:55 +08:00
Lightczx
ca9783bc1b corner radius on auto suggest tokenbox 2024-03-15 10:54:06 +08:00
DismissedLight
6e8e151fff fix unlocking fps 2024-03-14 20:32:06 +08:00
DismissedLight
b98dc9f5d3 fix unlocking fps 2024-03-14 19:58:01 +08:00
Masterain
206100d8ef New Crowdin updates (#1477) 2024-03-14 14:41:52 +08:00
Lightczx
1a74c7ca96 fix #1476 2024-03-14 11:24:55 +08:00
DismissedLight
88528fa28d hide zero count section 2024-03-13 21:18:11 +08:00
DismissedLight
263cea9225 Merge pull request #1475 from DGP-Studio/fix/token_ui 2024-03-13 21:09:13 +08:00
Lightczx
2879bd653a add chronicled wish cloud statistics 2024-03-13 17:25:42 +08:00
36 changed files with 223 additions and 365 deletions

View File

@@ -29,6 +29,7 @@
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///View/Card/Primitive/CardProgressBar.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style

View File

@@ -1,8 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.Control.AutoSuggestBox;
@@ -20,6 +23,20 @@ internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
TokenItemAdding += OnTokenItemAdding;
TokenItemAdded += OnTokenItemModified;
TokenItemRemoved += OnTokenItemModified;
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (this.FindDescendant("SuggestionsPopup") is Popup { Child: Border { Child: ListView listView } border })
{
IAppResourceProvider appResourceProvider = Ioc.Default.GetRequiredService<IAppResourceProvider>();
listView.Background = null;
listView.Margin = appResourceProvider.GetResource<Thickness>("AutoSuggestListPadding");
border.Background = appResourceProvider.GetResource<Microsoft.UI.Xaml.Media.Brush>("AutoSuggestBoxSuggestionsListBackground");
border.CornerRadius = new(0, 0, 8, 8);
}
}
private void OnFilterSuggestionRequested(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
@@ -32,9 +49,6 @@ internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
{
sender.ItemsSource = AvailableTokens.Values.Where(q => q.Value.Contains(Text, StringComparison.OrdinalIgnoreCase));
// TODO: CornerRadius
// Popup? popup = this.FindDescendant("SuggestionsPopup") as Popup;
}
}
@@ -45,7 +59,7 @@ internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
return;
}
CommandExtension.TryExecute(FilterCommand, FilterCommandParameter);
CommandInvocation.TryExecute(FilterCommand, FilterCommandParameter);
}
private void OnTokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
@@ -60,6 +74,6 @@ internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
private void OnTokenItemModified(TokenizingTextBox sender, object args)
{
CommandExtension.TryExecute(FilterCommand, FilterCommandParameter);
CommandInvocation.TryExecute(FilterCommand, FilterCommandParameter);
}
}

View File

@@ -3,7 +3,7 @@
namespace Snap.Hutao.Control.Extension;
internal static class CommandExtension
internal static class CommandInvocation
{
public static bool TryExecute(this ICommand? command, object? parameter = null)
{

View File

@@ -2,11 +2,13 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Control.Extension;
internal static class DependencyObjectExtension
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IServiceProvider ServiceProvider(this DependencyObject obj)
{
return Ioc.Default;

View File

@@ -3,7 +3,9 @@
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.ExceptionService;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Image;
@@ -26,12 +28,11 @@ internal sealed class CachedImage : Implementation.ImageEx
/// <inheritdoc/>
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
// We can only use Ioc to retrieve IImageCache, no IServiceProvider is available.
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
try
{
Verify.Operation(!string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), HutaoExceptionKind.ImageCacheInvalidUri, SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.

View File

@@ -7,21 +7,14 @@ using Windows.Media.Casting;
namespace Snap.Hutao.Control.Image.Implementation;
internal class ImageEx : ImageExBase
[DependencyProperty("NineGrid", typeof(Thickness))]
internal partial class ImageEx : ImageExBase
{
private static readonly DependencyProperty NineGridProperty = DependencyProperty.Register(nameof(NineGrid), typeof(Thickness), typeof(ImageEx), new PropertyMetadata(default(Thickness)));
public ImageEx()
: base()
{
}
public Thickness NineGrid
{
get => (Thickness)GetValue(NineGridProperty);
set => SetValue(NineGridProperty, value);
}
public override CompositionBrush GetAlphaMask()
{
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)

View File

@@ -6,6 +6,7 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Win32;
using System.IO;
using Windows.Foundation;
@@ -158,7 +159,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
if (value)
{
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
control.InvalidateLazyLoading();
}
else
@@ -169,7 +169,7 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
private static void LazyLoadingThresholdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageExBase control && control.EnableLazyLoading)
if (d is ImageExBase { EnableLazyLoading: true } control)
{
control.InvalidateLazyLoading();
}
@@ -229,9 +229,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
private void AttachPlaceholderSource(ImageSource? source)
{
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
// We only need to call those methods if we fail in other cases before we get here.
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
{
image.Source = source;
@@ -240,6 +237,15 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
{
brush.ImageSource = source;
}
if (source is null)
{
VisualStateManager.GoToState(this, UnloadedState, true);
}
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
{
VisualStateManager.GoToState(this, LoadedState, true);
}
}
private async void SetSource(object? source)
@@ -311,8 +317,7 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
}
tokenSource?.Cancel();
tokenSource = new CancellationTokenSource();
tokenSource = new();
AttachPlaceholderSource(null);
@@ -443,9 +448,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return;
}
Rect controlRect = TransformToVisual(hostElement)
.TransformBounds(new Rect(0, 0, ActualWidth, ActualHeight));
Rect controlRect = TransformToVisual(hostElement).TransformBounds(StructMarshal.Rect(ActualSize));
double lazyLoadingThreshold = LazyLoadingThreshold;
// Left/Top 1 Threshold, Right/Bottom 2 Threshold
Rect hostRect = new(
0 - lazyLoadingThreshold,
0 - lazyLoadingThreshold,

View File

@@ -1,151 +0,0 @@
// 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 = [];
List<ItemCollectionTransition> removeTransitions = [];
List<ItemCollectionTransition> moveTransitions = [];
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

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Specialized;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Foundation;
@@ -18,14 +19,12 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
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/>
@@ -82,16 +81,10 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
(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));
double totalWidth = ((state.ColumnWidth + MinColumnSpacing) * numberOfColumns) - MinColumnSpacing;
if (totalWidth > availableWidth)
{
numberOfColumns--;
@@ -103,7 +96,6 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
if (numberOfColumns != state.NumberOfColumns)
{
// The items will not need to be remeasured, but they will need to go into new columns
state.ClearColumns();
}
@@ -170,7 +162,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
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
// this item changed size; we need to recalculate layout for everything after this item
state.RemoveFromIndex(i + 1);
item.Height = item.Element.DesiredSize.Height;
columnHeights[columnIndex] = item.Top + item.Height;
@@ -201,16 +193,16 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
// 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);
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(layout))
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(state.GetColumnLayout(columnIndex)))
{
double bottom = item.Top + item.Height;
if (bottom < context.RealizationRect.Top)
{
// element is above the realization bounds
// Element is above the realization bounds
continue;
}
// Partial or fully in the view
if (item.Top <= context.RealizationRect.Bottom)
{
double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
@@ -229,21 +221,22 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
return finalSize;
}
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double minColumnSpacing)
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double columnSpacing)
{
// test if the width can fit in 2 items
if ((2 * minItemWidth) + minColumnSpacing > availableWidth)
if ((2 * minItemWidth) + columnSpacing > availableWidth)
{
return (1, availableWidth);
}
int columnCount = Math.Max(1, (int)((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing)));
double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount;
return (columnCount, columnWidthAddSpacing - minColumnSpacing);
int columnCount = Math.Max(1, (int)((availableWidth + columnSpacing) / (minItemWidth + columnSpacing)));
double columnWidthWithSpacing = (availableWidth + columnSpacing) / columnCount;
return (columnCount, columnWidthWithSpacing - columnSpacing);
}
private static int GetLowestColumnIndex(in ReadOnlySpan<double> columnHeights)
{
// We want to find the leftest column with the lowest height
int columnIndex = 0;
double height = columnHeights[0];
for (int j = 1; j < columnHeights.Length; j++)
@@ -260,13 +253,11 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
panel.InvalidateMeasure();
((UniformStaggeredLayout)d).InvalidateMeasure();
}
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
panel.InvalidateMeasure();
((UniformStaggeredLayout)d).InvalidateMeasure();
}
}

View File

@@ -1,7 +1,6 @@
// 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;
@@ -67,46 +66,6 @@ internal sealed class UniformStaggeredLayoutState
return columnLayout[columnIndex];
}
/// <summary>
/// Clear everything that has been calculated.
/// </summary>
internal void Clear()
{
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
// The first element must be force refreshed otherwise
// it will use the old one realized
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Now we need to refresh the first element of each column
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Finally we need to refresh the whole layout when we reset
if (context.ItemCount > 0)
{
for (int i = 0; i < context.ItemCount; i++)
{
RecycleElementAt(i);
}
}
columnLayout.Clear();
items.Clear();
}
/// <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);
@@ -139,10 +98,37 @@ internal sealed class UniformStaggeredLayoutState
return desiredHeight;
}
internal void Clear()
{
RecycleElements();
ClearColumns();
ClearItems();
}
internal void ClearColumns()
{
columnLayout.Clear();
}
internal void ClearItems()
{
items.Clear();
}
internal void RecycleElements()
{
if (context.ItemCount > 0)
{
for (int i = 0; i < items.Count; i++)
{
RecycleElementAt(i);
}
}
}
internal void RecycleElementAt(int index)
{
UIElement element = context.GetOrCreateElementAt(index);
context.RecycleElement(element);
context.RecycleElement(context.GetOrCreateElementAt(index));
}
internal void RemoveFromIndex(int index)
@@ -175,7 +161,7 @@ internal sealed class UniformStaggeredLayoutState
{
for (int i = startIndex; i <= endIndex; i++)
{
if (i > items.Count)
if (i >= items.Count)
{
break;
}
@@ -184,7 +170,7 @@ internal sealed class UniformStaggeredLayoutState
item.Height = 0;
item.Top = 0;
// We must recycle all elements to ensure that it gets the correct context
// We must recycle all removed elements to ensure that it gets the correct context
RecycleElementAt(i);
}

View File

@@ -72,7 +72,11 @@ internal class ScopedPage : Page
DisposeViewModel();
}
DataContext = null;
if (this.IsDisposed())
{
return;
}
Unloaded -= unloadEventHandler;
}

View File

@@ -9,6 +9,7 @@ internal enum HutaoExceptionKind
// Foundation
ServiceTypeCastFailed,
ImageCacheInvalidUri,
// IO
FileSystemCreateFileInsufficientPermissions,

View File

@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Service;
namespace Snap.Hutao.Factory.ContentDialog;
@@ -15,6 +16,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
private readonly ICurrentWindowReference currentWindowReference;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
/// <inheritdoc/>
public async ValueTask<ContentDialogResult> CreateForConfirmAsync(string title, string content)
@@ -27,6 +29,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
Content = content,
DefaultButton = ContentDialogButton.Primary,
PrimaryButtonText = SH.ContentDialogConfirmPrimaryButtonText,
RequestedTheme = appOptions.ElementTheme,
};
return await dialog.ShowAsync();
@@ -44,6 +47,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
DefaultButton = defaultButton,
PrimaryButtonText = SH.ContentDialogConfirmPrimaryButtonText,
CloseButtonText = SH.ContentDialogCancelCloseButtonText,
RequestedTheme = appOptions.ElementTheme,
};
return await dialog.ShowAsync();
@@ -58,6 +62,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
XamlRoot = currentWindowReference.GetXamlRoot(),
Title = title,
Content = new ProgressBar() { IsIndeterminate = true },
RequestedTheme = appOptions.ElementTheme,
};
return dialog;
@@ -69,6 +74,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
await taskContext.SwitchToMainThreadAsync();
TContentDialog contentDialog = serviceProvider.CreateInstance<TContentDialog>(parameters);
contentDialog.XamlRoot = currentWindowReference.GetXamlRoot();
contentDialog.RequestedTheme = appOptions.ElementTheme;
return contentDialog;
}
@@ -77,6 +83,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
{
TContentDialog contentDialog = serviceProvider.CreateInstance<TContentDialog>(parameters);
contentDialog.XamlRoot = currentWindowReference.GetXamlRoot();
contentDialog.RequestedTheme = appOptions.ElementTheme;
return contentDialog;
}
}

View File

@@ -2922,7 +2922,7 @@
<value>〓Event Duration〓.*?\d\.\d Available throughout the entirety of Version</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓Event Duration〓|Event Wish Duration|【Availability Duration】).*?(\d\.\dAfter the Version update).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
<value>(?:〓Event Duration〓|Event Wish Duration|【Availability Duration】|〓Discount Period〓).*?(\d\.\dAfter the Version update).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓Update Maintenance Duration〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -2247,7 +2247,7 @@
<value>Berkas</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程联动</value>
<value>InterProcess</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>Menjalankan perangkat lunak pada layar yang dipilih</value>

View File

@@ -2247,7 +2247,7 @@
<value>ファイル</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程联动</value>
<value>インタープロセス</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>指定したディスプレイで実行</value>
@@ -2922,7 +2922,7 @@
<value>〓イベント期間〓.*?\d\.\d当バージョン期間オープン</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
<value>(?:〓イベント期間〓|祈願期間|【開始日時】).*?(\d\.\dバージョンアップ完了後).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓メンテナンス時間〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -2247,7 +2247,7 @@
<value>文件</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程联动</value>
<value>进程</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>지정한 모니터에서 실행</value>
@@ -2922,7 +2922,7 @@
<value>〓活动时间〓.*?\d\.\d版本期间持续开放</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
<value>(?:〓活动时间〓|祈愿时间|【上架时间】).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -2247,7 +2247,7 @@
<value>Arquivo</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程联动</value>
<value>Comunicações entre processos</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>Executar o software na tela selecionada</value>
@@ -2922,7 +2922,7 @@
<value>〓Duração do evento〓.*?\d\.\d Disponível em toda as versões</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
<value>(?:〓Duração do evento〓|Duração da oração do evento|【Duração da disponibilidade】).*?(\d\.\dApós a atualização da versão).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓Duração da manutenção da atualização.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -2247,7 +2247,7 @@
<value>文件</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程联动</value>
<value>进程</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>在指定的显示器上运行</value>
@@ -2922,7 +2922,7 @@
<value>〓活动时间〓.*?\d\.\d版本期间持续开放</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
<value>(?:〓活动时间〓|祈愿时间|【上架时间】).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -17,7 +17,7 @@ internal sealed class LaunchStatus
public string Description { get; set; }
public static LaunchStatus FromUnlockState(GameFpsUnlockerState unlockerState)
public static LaunchStatus FromUnlockerContext(GameFpsUnlockerContext unlockerState)
{
if (unlockerState.FindModuleResult == FindModuleResult.Ok)
{

View File

@@ -9,7 +9,7 @@ internal sealed class LaunchExecutionBetterGenshinImpactAutomationHandlder : ILa
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
if (context.Options.UseBetterGenshinImpactAutomation)
if (!context.Process.HasExited && context.Options.UseBetterGenshinImpactAutomation)
{
context.Logger.LogInformation("Using BetterGI to automate gameplay");
await LaunchBetterGenshinImpactAsync(context).ConfigureAwait(false);

View File

@@ -9,7 +9,7 @@ internal sealed class LaunchExecutionStarwardPlayTimeStatisticsHandler : ILaunch
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
if (context.Options.UseStarwardPlayTimeStatistics)
if (!context.Process.HasExited && context.Options.UseStarwardPlayTimeStatistics)
{
context.Logger.LogInformation("Using Starward to count game time");
await LaunchStarwardForPlayTimeStatisticsAsync(context).ConfigureAwait(false);

View File

@@ -18,15 +18,17 @@ internal sealed class LaunchExecutionUnlockFpsHandler : ILaunchExecutionDelegate
context.Progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService<IProgressFactory>();
IProgress<GameFpsUnlockerState> progress = progressFactory.CreateForMainThread<GameFpsUnlockerState>(status => context.Progress.Report(LaunchStatus.FromUnlockState(status)));
IProgress<GameFpsUnlockerContext> progress = progressFactory.CreateForMainThread<GameFpsUnlockerContext>(c => context.Progress.Report(LaunchStatus.FromUnlockerContext(c)));
GameFpsUnlocker unlocker = new(context.ServiceProvider, context.Process, new(100, 20000, 3000), progress);
try
{
await unlocker.UnlockAsync(context.CancellationToken).ConfigureAwait(false);
unlocker.PostUnlockAsync(context.CancellationToken).SafeForget();
if (await unlocker.UnlockAsync(context.CancellationToken).ConfigureAwait(false))
{
unlocker.PostUnlockAsync(context.CancellationToken).SafeForget();
}
}
catch (InvalidOperationException ex)
catch (Exception ex)
{
context.Logger.LogCritical(ex, "Unlocking FPS failed");

View File

@@ -16,7 +16,7 @@ internal static class GameFpsAddress
private const byte ASM_JMP = 0xE9;
#pragma warning restore SA1310
public static unsafe void UnsafeFindFpsAddress(GameFpsUnlockerState state, in RequiredGameModule requiredGameModule)
public static unsafe void UnsafeFindFpsAddress(GameFpsUnlockerContext state, in RequiredGameModule requiredGameModule)
{
bool readOk = UnsafeReadModulesMemory(state.GameProcess, requiredGameModule, out VirtualMemory localMemory);
HutaoException.ThrowIfNot(readOk, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerReadModuleMemoryCopyVirtualMemoryFailed);

View File

@@ -16,45 +16,46 @@ namespace Snap.Hutao.Service.Game.Unlocker;
internal sealed class GameFpsUnlocker : IGameFpsUnlocker
{
private readonly LaunchOptions launchOptions;
private readonly GameFpsUnlockerState state = new();
private readonly GameFpsUnlockerContext context = new();
public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess, in UnlockTimingOptions options, IProgress<GameFpsUnlockerState> progress)
public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess, in UnlockTimingOptions options, IProgress<GameFpsUnlockerContext> progress)
{
launchOptions = serviceProvider.GetRequiredService<LaunchOptions>();
state.GameProcess = gameProcess;
state.TimingOptions = options;
state.Progress = progress;
context.GameProcess = gameProcess;
context.TimingOptions = options;
context.Progress = progress;
}
/// <inheritdoc/>
public async ValueTask UnlockAsync(CancellationToken token = default)
public async ValueTask<bool> UnlockAsync(CancellationToken token = default)
{
HutaoException.ThrowIfNot(state.IsUnlockerValid, HutaoExceptionKind.GameFpsUnlockingFailed, "This Unlocker is invalid");
(FindModuleResult result, RequiredGameModule gameModule) = await GameProcessModule.FindModuleAsync(state).ConfigureAwait(false);
HutaoException.ThrowIfNot(context.IsUnlockerValid, HutaoExceptionKind.GameFpsUnlockingFailed, "This Unlocker is invalid");
(FindModuleResult result, RequiredGameModule gameModule) = await GameProcessModule.FindModuleAsync(context).ConfigureAwait(false);
HutaoException.ThrowIfNot(result != FindModuleResult.TimeLimitExeeded, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded);
HutaoException.ThrowIfNot(result != FindModuleResult.NoModuleFound, HutaoExceptionKind.GameFpsUnlockingFailed, SH.ServiceGameUnlockerFindModuleNoModuleFound);
GameFpsAddress.UnsafeFindFpsAddress(state, gameModule);
state.Report();
GameFpsAddress.UnsafeFindFpsAddress(context, gameModule);
context.Report();
return context.FpsAddress != 0U;
}
public async ValueTask PostUnlockAsync(CancellationToken token = default)
{
using (PeriodicTimer timer = new(state.TimingOptions.AdjustFpsDelay))
using (PeriodicTimer timer = new(context.TimingOptions.AdjustFpsDelay))
{
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
if (!state.GameProcess.HasExited && state.FpsAddress != 0U)
if (!context.GameProcess.HasExited && context.FpsAddress != 0U)
{
UnsafeWriteProcessMemory(state.GameProcess, state.FpsAddress, launchOptions.TargetFps);
state.Report();
UnsafeWriteProcessMemory(context.GameProcess, context.FpsAddress, launchOptions.TargetFps);
context.Report();
}
else
{
state.IsUnlockerValid = false;
state.FpsAddress = 0;
state.Report();
context.IsUnlockerValid = false;
context.FpsAddress = 0;
context.Report();
return;
}
}

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary>
/// 解锁状态
/// </summary>
internal sealed class GameFpsUnlockerState
internal sealed class GameFpsUnlockerContext
{
public string Description { get; set; } = default!;
@@ -22,7 +22,7 @@ internal sealed class GameFpsUnlockerState
public Process GameProcess { get; set; } = default!;
public IProgress<GameFpsUnlockerState> Progress { get; set; } = default!;
public IProgress<GameFpsUnlockerContext> Progress { get; set; } = default!;
public void Report()
{

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Service.Game.Unlocker;
internal static class GameProcessModule
{
public static async ValueTask<ValueResult<FindModuleResult, RequiredGameModule>> FindModuleAsync(GameFpsUnlockerState state)
public static async ValueTask<ValueResult<FindModuleResult, RequiredGameModule>> FindModuleAsync(GameFpsUnlockerContext state)
{
ValueStopwatch watch = ValueStopwatch.StartNew();
using (PeriodicTimer timer = new(state.TimingOptions.FindModuleDelay))

View File

@@ -11,5 +11,5 @@ internal interface IGameFpsUnlocker
{
ValueTask PostUnlockAsync(CancellationToken token = default);
ValueTask UnlockAsync(CancellationToken token = default);
ValueTask<bool> UnlockAsync(CancellationToken token = default);
}

View File

@@ -302,8 +302,8 @@
<PackageReference Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.0.240109" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.0.240109" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.2">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -311,14 +311,14 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.1" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.2.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240227000" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240311000" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.6.0" />
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.15.3">

View File

@@ -70,7 +70,7 @@
Maximum="{Binding GuaranteeOrangeThreshold}"
ProgressForeground="{StaticResource OrangeColorBrush}"
TextForeground="{StaticResource OrangeColorBrush}"
Value="{Binding LastOrangePull}"/>
Value="{Binding LastOrangePull, Mode=OneWay}"/>
<shvcp:CardProgressBar
Grid.Column="0"
Description="{Binding LastPurplePull}"

View File

@@ -13,12 +13,9 @@ namespace Snap.Hutao.View.Card.Primitive;
[DependencyProperty("Value", typeof(double))]
[DependencyProperty("Header", typeof(string))]
[DependencyProperty("Description", typeof(string))]
internal sealed partial class CardProgressBar : Grid
internal sealed partial class CardProgressBar : ContentControl
{
public CardProgressBar()
{
IAppResourceProvider appResourceProvider = Ioc.Default.GetRequiredService<IAppResourceProvider>();
TextForeground = appResourceProvider.GetResource<Brush>("TextFillColorPrimaryBrush");
InitializeComponent();
}
}

View File

@@ -1,39 +1,49 @@
<Grid
x:Class="Snap.Hutao.View.Card.Primitive.CardProgressBar"
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
MinHeight="40"
Style="{ThemeResource GridCardStyle}"
mc:Ignorable="d">
xmlns:shvcp="using:Snap.Hutao.View.Card.Primitive">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ProgressBar
Grid.ColumnSpan="2"
MinHeight="{x:Bind MinHeight, Mode=OneWay}"
Background="Transparent"
CornerRadius="{StaticResource ControlCornerRadius}"
Foreground="{x:Bind ProgressForeground, Mode=OneWay}"
Maximum="{x:Bind Maximum, Mode=OneWay}"
Opacity="{StaticResource LargeBackgroundProgressBarOpacity}"
Value="{x:Bind Value, Mode=OneWay}"/>
<TextBlock
Grid.Column="0"
Margin="6,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{x:Bind TextForeground, Mode=OneWay}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Header, Mode=OneWay}"/>
<TextBlock
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{x:Bind TextForeground, Mode=OneWay}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Description, Mode=OneWay}"/>
</Grid>
<Style BasedOn="{StaticResource DefaultCardProgressBarStyle}" TargetType="shvcp:CardProgressBar"/>
<Style x:Key="DefaultCardProgressBarStyle" TargetType="shvcp:CardProgressBar">
<Setter Property="TextForeground" Value="{ThemeResource TextFillColorPrimaryBrush}"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid MinHeight="{TemplateBinding MinHeight}" Style="{ThemeResource GridCardStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ProgressBar
Grid.ColumnSpan="2"
MinHeight="{TemplateBinding MinHeight}"
Background="Transparent"
CornerRadius="{StaticResource ControlCornerRadius}"
Foreground="{Binding ProgressForeground, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{Binding Maximum, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
Opacity="{StaticResource LargeBackgroundProgressBarOpacity}"
Value="{Binding Value, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"/>
<TextBlock
Grid.Column="0"
Margin="6,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{Binding TextForeground, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Header, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"/>
<TextBlock
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{Binding TextForeground, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Description, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -52,41 +52,49 @@
Margin="0,16,0,8"
Foreground="{ThemeResource UpPullColorBrush}"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewControlStatisticsCardUpText}"/>
Text="{shcm:ResourceString Name=ViewControlStatisticsCardUpText}"
Visibility="{Binding UpItems.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
<ItemsControl
ItemTemplate="{StaticResource GridTemplate}"
ItemsPanel="{StaticResource WrapPanelSpacing4Template}"
ItemsSource="{Binding UpItems}"/>
ItemsSource="{Binding UpItems}"
Visibility="{Binding UpItems.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
<TextBlock
Margin="0,16,0,8"
Foreground="{ThemeResource OrangeColorBrush}"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewControlStatisticsCardOrangeText}"/>
Text="{shcm:ResourceString Name=ViewControlStatisticsCardOrangeText}"
Visibility="{Binding OrangeItems.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
<ItemsControl
ItemTemplate="{StaticResource GridTemplate}"
ItemsPanel="{StaticResource WrapPanelSpacing4Template}"
ItemsSource="{Binding OrangeItems}"/>
ItemsSource="{Binding OrangeItems}"
Visibility="{Binding OrangeItems.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
<TextBlock
Margin="0,16,0,8"
Foreground="{ThemeResource PurpleColorBrush}"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewControlStatisticsCardPurpleText}"/>
Text="{shcm:ResourceString Name=ViewControlStatisticsCardPurpleText}"
Visibility="{Binding PurpleItems.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
<ItemsControl
ItemTemplate="{StaticResource GridTemplate}"
ItemsPanel="{StaticResource WrapPanelSpacing4Template}"
ItemsSource="{Binding PurpleItems}"/>
ItemsSource="{Binding PurpleItems}"
Visibility="{Binding PurpleItems.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
<TextBlock
Margin="0,16,0,8"
Foreground="{ThemeResource BlueColorBrush}"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewControlStatisticsCardBlueText}"/>
Text="{shcm:ResourceString Name=ViewControlStatisticsCardBlueText}"
Visibility="{Binding BlueItems.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
<ItemsControl
ItemTemplate="{StaticResource GridTemplate}"
ItemsPanel="{StaticResource WrapPanelSpacing4Template}"
ItemsSource="{Binding BlueItems}"/>
ItemsSource="{Binding BlueItems}"
Visibility="{Binding BlueItems.Count, Converter={StaticResource Int32ToVisibilityConverter}}"/>
</StackPanel>
</ScrollViewer>
</Grid>

View File

@@ -479,10 +479,12 @@
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shvc:HutaoStatisticsCard Grid.Column="0" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.AvatarEvent}"/>
<shvc:HutaoStatisticsCard Grid.Column="1" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.AvatarEvent2}"/>
<shvc:HutaoStatisticsCard Grid.Column="2" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.WeaponEvent}"/>
<shvc:HutaoStatisticsCard Grid.Column="3" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.Chronicled}"/>
</Grid>
</Grid>
</PivotItem>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using System.Text;
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
@@ -16,6 +17,6 @@ internal static class LatestPackageExtension
}
latest.Path = pathBuilder.ToStringTrimEndReturn();
latest.Name = latest.Segments[0].Path[..^4]; // .00X
latest.Name = Path.GetFileName(latest.Segments[0].Path[..^4]); // .00X
}
}

View File

@@ -4,6 +4,7 @@
using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices;
using Windows.Foundation;
using Windows.Graphics;
namespace Snap.Hutao.Win32;
@@ -26,45 +27,26 @@ internal static class StructMarshal
return color;
}
/// <summary>
/// 从 0,0 点构造一个新的<see cref="Windows.Graphics.RectInt32"/>
/// </summary>
/// <param name="size">尺寸</param>
/// <returns>新的实例</returns>
public static Rect Rect(Vector2 size)
{
return new(0, 0, size.X, size.Y);
}
public static RectInt32 RectInt32(SizeInt32 size)
{
return new(0, 0, size.Width, size.Height);
}
/// <summary>
/// 构造一个新的<see cref="Windows.Graphics.RectInt32"/>
/// </summary>
/// <param name="point">左上角位置</param>
/// <param name="size">尺寸</param>
/// <returns>新的实例</returns>
public static RectInt32 RectInt32(PointInt32 point, Vector2 size)
{
return RectInt32(point.X, point.Y, size);
}
/// <summary>
/// 构造一个新的<see cref="Windows.Graphics.RectInt32"/>
/// </summary>
/// <param name="x">X</param>
/// <param name="y">Y</param>
/// <param name="size">尺寸</param>
/// <returns>新的实例</returns>
public static RectInt32 RectInt32(int x, int y, Vector2 size)
{
return new(x, y, (int)size.X, (int)size.Y);
}
/// <summary>
/// 构造一个新的<see cref="Windows.Graphics.RectInt32"/>
/// </summary>
/// <param name="point">左上角位置</param>
/// <param name="size">尺寸</param>
/// <returns>新的实例</returns>
public static RectInt32 RectInt32(PointInt32 point, SizeInt32 size)
{
return new(point.X, point.Y, size.Width, size.Height);