Compare commits

...

25 Commits

Author SHA1 Message Date
DismissedLight
bb01f3a3cb fix package convert issue 2023-01-30 10:43:05 +08:00
DismissedLight
f7f2d9c867 fix #406 2023-01-28 20:03:37 +08:00
DismissedLight
01b7e58b3e fix convert cache 2023-01-27 16:51:43 +08:00
DismissedLight
2518ae0b90 package convert impl 2023-01-27 11:22:25 +08:00
DismissedLight
7d4a8cdcd9 fix empty statistics [skip ci] 2023-01-23 13:06:56 +08:00
DismissedLight
623893e00e remove visual transition gap in gacha log initialization 2023-01-23 12:58:00 +08:00
Masterain
0d34c81bcf Merge pull request #388 from wordlesswind/patch-1
Update version information and fix broken links
2023-01-22 01:07:49 -08:00
清靈語
5f3d0126b3 Update version information 2023-01-22 12:51:57 +08:00
DismissedLight
5d1fe3f38a move dispatcher queue to thread helper 2023-01-21 13:14:54 +08:00
DismissedLight
c810ffa625 remove unnecessary converters 2023-01-20 17:30:16 +08:00
DismissedLight
ee70205245 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-01-20 15:47:18 +08:00
DismissedLight
06c8b347d3 Announcement Viewer 2023-01-20 15:47:05 +08:00
Masterain
5c6ab1dee9 Update azure-pipelines.yml for Azure Pipelines 2023-01-19 15:23:50 -08:00
DismissedLight
ad440e0561 fix #377 2023-01-19 14:55:53 +08:00
DismissedLight
ca56d8c636 remove async relay command factory 2023-01-18 15:29:22 +08:00
Masterain
da0ee0cca6 Update PublishDistribution.yml
[skip ci]
2023-01-16 13:35:57 -08:00
Masterain
5d00d9cc0d Update azure-pipelines.yml for Azure Pipelines
[force ci]
2023-01-16 13:15:05 -08:00
Masterain
e8b27e6655 Update azure-pipelines.yml for Azure Pipelines 2023-01-16 13:05:55 -08:00
DismissedLight
0ac79012d1 fix #368 2023-01-16 18:12:12 +08:00
DismissedLight
bb2665b75e Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-01-16 14:27:45 +08:00
DismissedLight
d22ac39c1d fix dupe download items [skip ci] 2023-01-16 14:27:31 +08:00
Masterain
a312603d61 Update azure-pipelines.yml for Azure Pipelines 2023-01-15 22:22:34 -08:00
DismissedLight
0732ea0e06 replace font 2023-01-16 14:10:28 +08:00
Masterain
e4d2b3055c Update azure-pipelines.yml for Azure Pipelines
[skip ci]
2023-01-14 17:28:43 -08:00
Masterain
5668931230 Update PublishDistribution.yml
[skip ci]
2023-01-14 17:08:50 -08:00
167 changed files with 3530 additions and 1618 deletions

View File

@@ -15,7 +15,7 @@ body:
description: |-
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
options:
- label: 我已完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/most-frequent-questions.html),并认为我的问题没有在文档中得到解答
- label: 我已完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/),并认为我的问题没有在文档中得到解答
required: true
- label: 我使用的操作系统是[受支持的版本](https://hut.ao/quick-start.html#%E6%9C%80%E4%BD%8E%E7%B3%BB%E7%BB%9F%E8%A6%81%E6%B1%82)
@@ -33,7 +33,7 @@ body:
label: Windows 版本
description: |
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
placeholder: 22000.556
placeholder: 22621.1105
validations:
required: true
@@ -42,7 +42,7 @@ body:
attributes:
label: Snap Hutao 版本
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
placeholder: 1.1.0
placeholder: 1.3.13.0
validations:
required: true

View File

@@ -2,7 +2,7 @@ name: PublishDistribution
on:
release:
types: [published]
types: [released]
workflow_dispatch:
@@ -39,3 +39,13 @@ jobs:
EOF
rclone copy ./release-download/* dgpODCN:/releases/
# Purge Patch System Cache
- name: Purge Patch
env:
PATCH_HOSTS: ${{ secrets.PATCH_HOSTS }}
PURGE_TOKEN: ${{ secrets.PURGE_TOKEN }}
PURGE_URL: ${{ secrets.PURGE_URL }}
run: |
sudo echo "$PATCH_HOSTS" | sudo tee -a /etc/hosts
curl --header "Authorization: token $PURGE_TOKEN" $PURGE_URL

View File

@@ -1,12 +1,13 @@
# [Snap.Hutao](https://hut.ao)
![](https://repository-images.githubusercontent.com/482734649/5f8cf574-2ef0-43e9-aa8d-6cf094b54dd9)
> 唷,找本堂主有何贵干呀?
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
# 特别感谢
### 原神组织与个人
* [HolographicHat](https://github.com/HolographicHat)
* [UIGF organization](https://uigf.org)

View File

@@ -91,7 +91,8 @@ steps:
"Package/Identity/@Publisher": "CN=DGP Studio CI",
"Package/Identity/@Version": "$(build_date).$(rev_number)",
"Package/Properties/DisplayName": "胡桃 Alpha",
"Package/Properties/PublisherDisplayName":"DGP Studio CI"
"Package/Properties/PublisherDisplayName":"DGP Studio CI",
"Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha"
}
- task: CmdLine@2
@@ -155,10 +156,11 @@ steps:
title: '$(build_date).$(rev_number)'
releaseNotesSource: 'inline'
releaseNotesInline: |
## 提示 (Hint)
发布版本由 CI 程序自动打包生成,属于 `Alpha` 测试版,仅用于开发调试和内部测试用途。使用该版本可能存在意料之外的风险,请仅在有明确用途的情况下使用该版本。
This release is a Alpha Testing version generated by CI program automatically in a purpose of debugging and interal testing. Using this release may have unexpected risk, please only use it when you know what you are doing.
## 普通用户请勿下载
该版本由 CI 程序自动打包生成 `Alpha` 测试版本,**仅供开发者测试使用**
普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
assets: |
$(Build.ArtifactStagingDirectory)/*
$(cerFile.secureFilePath)
@@ -167,12 +169,13 @@ steps:
changeLogType: 'commitBased'
- task: DownloadSecureFile@1
name: cerFile
name: RcloneConfigFile
displayName: Download Rclone Config
inputs:
secureFile: 'rclone.conf'
- task: rclone@1
displayName: Upload CI via Rclone
inputs:
arguments: 'copy $(Build.ArtifactStagingDirectory)/* downloadDGPCN:/releases/Alpha/'
configPath: '$(cerFile.secureFilePath)/rclone.conf'
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/Alpha/'
configPath: '$(RcloneConfigFile.secureFilePath)'

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Installer;
internal class Program
{
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Appx";
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock";
private const string ValueName = "AllowDevelopmentWithoutDevLicense";
public static async Task Main(string[] args)

View File

@@ -11,24 +11,24 @@
<ResourceDictionary.MergedDictionaries>
<muxc:XamlControlsResources/>
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/Generic.xaml"/>
<ResourceDictionary Source="Control/Theme/FontStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<Color x:Key="AvatarPropertyAddValueColor">#FF74BF00</Color>
<Color x:Key="CompatBackgroundColor">#FFF4F4F4</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="AvatarPropertyAddValueColor">#FF90E800</Color>
<Color x:Key="CompatBackgroundColor">#FF242424</Color>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<!-- Modify Window title bar color -->
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush"/>
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush"/>
<!-- Page Transparent Background -->
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush"/>
<!-- IconFont -->
<FontFamily x:Key="SymbolThemeFontFamily">ms-appx:///Resource/Font/Segoe Fluent Icons.ttf#Segoe Fluent Icons</FontFamily>
<!-- InfoBar Resource -->
<Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness>
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
@@ -44,8 +44,10 @@
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
<!-- OpenPaneLength -->
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
<x:Double x:Key="CompatSplitViewOpenPaneLength2">252</x:Double>
<GridLength x:Key="CompatGridLength2">252</GridLength>
<x:Double x:Key="CompatSplitViewOpenPaneLength2">268</x:Double>
<GridLength x:Key="CompatGridLength2">268</GridLength>
<!-- Brushes -->
<SolidColorBrush x:Key="AvatarPropertyAddValueBrush" Color="{ThemeResource AvatarPropertyAddValueColor}"/>
<!-- Uris -->
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie</x:String>
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
@@ -79,10 +81,6 @@
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
<shvc:EmptyCollectionToBoolConverter x:Key="EmptyCollectionToBoolConverter"/>
<shvc:EmptyCollectionToBoolRevertConverter x:Key="EmptyCollectionToBoolRevertConverter"/>
<shvc:EmptyCollectionToVisibilityConverter x:Key="EmptyCollectionToVisibilityConverter"/>
<shvc:EmptyCollectionToVisibilityRevertConverter x:Key="EmptyCollectionToVisibilityRevertConverter"/>
<shvc:EmptyObjectToBoolConverter x:Key="EmptyObjectToBoolConverter"/>
<shvc:EmptyObjectToBoolRevertConverter x:Key="EmptyObjectToBoolRevertConverter"/>
<shvc:EmptyObjectToVisibilityConverter x:Key="EmptyObjectToVisibilityConverter"/>
@@ -90,6 +88,7 @@
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
<!-- Styles -->
<Style
x:Key="LargeGridViewItemStyle"
BasedOn="{StaticResource DefaultGridViewItemStyle}"

View File

@@ -25,7 +25,6 @@ public partial class App : Application
/// Initializes the singleton application object.
/// </summary>
/// <param name="logger">日志器</param>
/// <param name="appCenter">App Center</param>
public App(ILogger<App> logger)
{
// load app resource

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.Xaml.Interactivity;
namespace Snap.Hutao.Control.Behavior;
/// <summary>
/// 打开附着的浮出控件操作
/// </summary>
internal class OpenAttachedFlyoutAction : DependencyObject, IAction
{
/// <inheritdoc/>
public object Execute(object sender, object parameter)
{
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
return null!;
}
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;
/// <summary>
/// 纵横比控件
/// </summary>
internal class AspectRatio : Microsoft.UI.Xaml.Controls.ContentControl
{
private static readonly DependencyProperty TargetWidthProperty = Property<AspectRatio>.Depend(nameof(TargetWidth), 1D);
private static readonly DependencyProperty TargetHeightProperty = Property<AspectRatio>.Depend(nameof(TargetHeight), 1D);
/// <summary>
/// 目标宽度
/// </summary>
public double TargetWidth
{
get => (double)GetValue(TargetWidthProperty);
set => SetValue(TargetWidthProperty, value);
}
/// <summary>
/// 目标高度
/// </summary>
public double TargetHeight
{
get => (double)GetValue(TargetHeightProperty);
set => SetValue(TargetHeightProperty, value);
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
double ratio = TargetWidth / TargetHeight;
double ratioAvailable = availableSize.Width / availableSize.Height;
// 更宽
if (ratioAvailable > ratio)
{
double newWidth = ratio * availableSize.Height;
return new Size(newWidth, availableSize.Height);
}
// 更高
else if (ratioAvailable < ratio)
{
double newHeight = availableSize.Width / ratio;
return new Size(availableSize.Width, newHeight);
}
return availableSize;
}
}

View File

@@ -16,7 +16,7 @@ namespace Snap.Hutao.Control;
[SuppressMessage("", "CA1001")]
public class ScopedPage : Page
{
private readonly CancellationTokenSource viewLoadingCancellationTokenSource = new();
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope serviceScope;
/// <summary>
@@ -37,7 +37,7 @@ public class ScopedPage : Page
where TViewModel : class, IViewModel
{
IViewModel viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
@@ -60,10 +60,10 @@ public class ScopedPage : Page
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
using (viewLoadingCancellationTokenSource)
using (viewCancellationTokenSource)
{
// Cancel tasks executed by the view model
viewLoadingCancellationTokenSource.Cancel();
// Cancel all tasks executed by the view model
viewCancellationTokenSource.Cancel();
IViewModel viewModel = (IViewModel)DataContext;
using (SemaphoreSlim locker = viewModel.DisposeLock)
@@ -79,14 +79,13 @@ public class ScopedPage : Page
}
/// <inheritdoc/>
[SuppressMessage("", "VSTHRD100")]
protected override async void OnNavigatedTo(NavigationEventArgs e)
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData extra)
{
await NotifyRecipentAsync(extra).ConfigureAwait(false);
NotifyRecipentAsync(extra).SafeForget();
}
}
}

View File

@@ -0,0 +1,210 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wsc="using:WinUICommunity.SettingsUI.Controls">
<FontFamily x:Key="MiSans">ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
<FontFamily x:Key="CascadiaMonoAndMiSans">ms-appx:///Resource/Font/CascadiaMono.ttf#Cascadia Mono, ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
<StaticResource x:Key="PivotHeaderItemFontFamily" ResourceKey="MiSans"/>
<StaticResource x:Key="ContentControlThemeFontFamily" ResourceKey="MiSans"/>
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock"/>
<Style x:Key="BaseTextBlockStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
<Setter Property="FontSize" Value="{StaticResource BodyTextBlockFontSize}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="LineStackingStrategy" Value="MaxHeight"/>
<Setter Property="TextLineBounds" Value="Full"/>
</Style>
<Style
x:Key="HeaderTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="46"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="SubheaderTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="34"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="TitleTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource TitleTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="SubtitleTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource SubtitleTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="BodyTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontWeight" Value="Normal"/>
</Style>
<Style
x:Key="CaptionTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource CaptionTextBlockFontSize}"/>
<Setter Property="FontWeight" Value="Normal"/>
</Style>
<Style
x:Key="BodyStrongTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource BodyStrongTextBlockFontSize}"/>
</Style>
<Style
x:Key="TitleLargeTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource TitleLargeTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="DisplayTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource DisplayTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}" TargetType="MenuFlyoutItem">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultMenuFlyoutSubItemStyle}" TargetType="MenuFlyoutSubItem">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style TargetType="InfoBar">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultSettingStyle}" TargetType="wsc:Setting"/>
<Style x:Key="DefaultSettingStyle" TargetType="wsc:Setting">
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Background" Value="{ThemeResource CardBackgroundBrush}"/>
<Setter Property="BorderThickness" Value="{ThemeResource CardBorderThickness}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Margin" Value="0,0,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="wsc:Setting">
<Grid
x:Name="RootGrid"
MinHeight="48"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<!-- Icon -->
<ColumnDefinition Width="*"/>
<!-- Header and subtitle -->
<ColumnDefinition Width="Auto"/>
<!-- Action control -->
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="IconPresenter"
MaxWidth="20"
Margin="2,0,18,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Icon}"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="20"
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
IsTextScaleFactorEnabled="False"/>
<StackPanel
Grid.Column="1"
Margin="0,0,16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<TextBlock
x:Name="HeaderPresenter"
VerticalAlignment="Center"
FontFamily="{StaticResource MiSans}"
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
Text="{TemplateBinding Header}"/>
<ContentPresenter
x:Name="DescriptionPresenter"
Content="{TemplateBinding Description}"
FontFamily="{StaticResource MiSans}"
FontSize="{StaticResource SecondaryTextFontSize}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="WrapWholeWords">
<ContentPresenter.Resources>
<Style BasedOn="{StaticResource CaptionTextBlockStyle}" TargetType="TextBlock">
<Style.Setters>
<Setter Property="TextWrapping" Value="WrapWholeWords"/>
</Style.Setters>
</Style>
<Style BasedOn="{StaticResource TextButtonStyle}" TargetType="HyperlinkButton">
<Style.Setters>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Padding" Value="0,0,0,0"/>
</Style.Setters>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</StackPanel>
<ContentPresenter
x:Name="ContentPresenter"
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding ActionContent}"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="HeaderPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="DescriptionPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 可异步初始化
/// </summary>
internal interface ISupportAsyncInitialization
{
/// <summary>
/// 是否已经初始化完成
/// </summary>
public bool IsInitialized { get; }
/// <summary>
/// 异步初始化
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>初始化任务</returns>
ValueTask<bool> InitializeAsync();
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 表示支持验证
/// </summary>
internal interface ISupportValidation
{
/// <summary>
/// 验证
/// </summary>
/// <returns>当前数据是否有效</returns>
public bool Validate();
}

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Logging;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO;
using System.Net;
@@ -24,7 +25,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
private static readonly ImmutableDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new()
{
[0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16),
@@ -32,13 +33,13 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
[3] = TimeSpan.FromSeconds(4),
[4] = TimeSpan.FromSeconds(16),
[5] = TimeSpan.FromSeconds(64),
}.ToImmutableDictionary();
};
private readonly ILogger logger;
// violate di rule
private readonly HttpClient httpClient;
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private string? baseFolder;
private string? cacheFolder;
@@ -100,11 +101,30 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
/// <inheritdoc/>
public async Task<string> GetFileFromCacheAsync(Uri uri)
{
string filePath = Path.Combine(GetCacheFolder(), GetCacheFileName(uri));
string fileName = GetCacheFileName(uri);
string filePath = Path.Combine(GetCacheFolder(), fileName);
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
{
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
TaskCompletionSource taskCompletionSource = new();
try
{
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
{
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
}
else
{
if (concurrentTasks.TryGetValue(fileName, out Task? task))
{
await task.ConfigureAwait(false);
}
}
}
finally
{
taskCompletionSource.TrySetResult();
}
}
return filePath;
@@ -191,7 +211,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
if (retryCount == 3)
{
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao, }.Uri;
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao }.Uri;
}
}
}

View File

@@ -4,19 +4,19 @@
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core.Convert;
namespace Snap.Hutao.Core;
/// <summary>
/// 支持Md5转换
/// </summary>
internal abstract class Md5Convert
internal static class Convert
{
/// <summary>
/// 获取字符串的MD5计算结果
/// </summary>
/// <param name="source">源字符串</param>
/// <returns>计算的结果</returns>
public static string ToHexString(string source)
public static string ToMd5HexString(string source)
{
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
return System.Convert.ToHexString(hash);

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Core.Convert;
using Snap.Hutao.Core.Json;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
@@ -31,7 +30,7 @@ internal static class CoreEnvironment
/// <summary>
/// 米游社 Rpc 版本
/// </summary>
public const string HoyolabXrpcVersion = "2.43.1";
public const string HoyolabXrpcVersion = "2.44.1";
/// <summary>
/// 盐
@@ -39,8 +38,8 @@ internal static class CoreEnvironment
// https://github.com/UIGF-org/Hoyolab.Salt
public static readonly ImmutableDictionary<string, string> DynamicSecrets = new Dictionary<string, string>()
{
[nameof(SaltType.K2)] = "ODzG1Jrn6zebX19VRmaJwjFI2CDvBUGq",
[nameof(SaltType.LK2)] = "V1PYbXKQY7ysdx3MNCcNbsE1LtY2QZpW",
[nameof(SaltType.K2)] = "dZAwGk4e9aC0MXXItkwnHamjA1x30IYw",
[nameof(SaltType.LK2)] = "IEIZiKYaput2OCKQprNuGsog1NZc1FkS",
[nameof(SaltType.X4)] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
[nameof(SaltType.X6)] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
[nameof(SaltType.PROD)] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
@@ -113,15 +112,15 @@ internal static class CoreEnvironment
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
return Md5Convert.ToHexString($"{userName}{machineGuid}");
return Convert.ToMd5HexString($"{userName}{machineGuid}");
}
private static string GetDocumentsHutaoPath()
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";

View File

@@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 数据库集合上下文
/// 数据库集合扩展
/// </summary>
public static class DbSetExtension
{
@@ -134,4 +134,4 @@ public static class DbSetExtension
dbSet.Update(entity);
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 可查询扩展
/// </summary>
public static class QueryableExtension
{
/// <summary>
/// source.Where(predicate).ExecuteDeleteAsync(token)
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <param name="predicate">条件</param>
/// <param name="token">取消令牌</param>
/// <returns>SQL返回个数</returns>
public static Task<int> ExecuteDeleteWhereAsync<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate, CancellationToken token = default)
{
return source.Where(predicate).ExecuteDeleteAsync(token);
}
}

View File

@@ -38,7 +38,9 @@ internal static class IocConfiguration
{
if (context.Database.GetPendingMigrations().Any())
{
#if DEBUG
Debug.WriteLine("[Debug] Performing AppDbContext Migrations");
#endif
context.Database.Migrate();
}
}

View File

@@ -10,7 +10,8 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// </summary>
public static class ServiceScopeExtension
{
private static IServiceScope? scopeReference;
// Allow GC to Collect the IServiceScope
private static readonly WeakReference<IServiceScope> ScopeReference = new(null!);
/// <summary>
/// 追踪服务范围
@@ -19,7 +20,7 @@ public static class ServiceScopeExtension
public static void Track(this IServiceScope scope)
{
DisposeLast();
scopeReference = scope;
ScopeReference.SetTarget(scope);
}
/// <summary>
@@ -27,6 +28,9 @@ public static class ServiceScopeExtension
/// </summary>
public static void DisposeLast()
{
scopeReference?.Dispose();
if (ScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
}
}
}

View File

@@ -45,4 +45,4 @@ internal class ExceptionRecorder
{
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
}
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 用户数据损坏异常
/// </summary>
internal class UserdataCorruptedException : Exception
{
/// <summary>
/// 构造一个新的用户数据损坏异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public UserdataCorruptedException(string message, Exception innerException)
: base($"用户数据已损坏: {message}", innerException)
{
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using System.Security.Cryptography;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 摘要
/// </summary>
internal static class Digest
{
/// <summary>
/// 异步获取文件 Md5 摘要
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="token">取消令牌</param>
/// <returns>文件 Md5 摘要</returns>
public static async Task<string> GetFileMd5Async(string filePath, CancellationToken token = default)
{
using (FileStream stream = File.OpenRead(filePath))
{
return await GetStreamMd5Async(stream, token).ConfigureAwait(false);
}
}
/// <summary>
/// 获取流的 Md5 摘要
/// </summary>
/// <param name="stream">流</param>
/// <param name="token">取消令牌</param>
/// <returns>流 Md5 摘要</returns>
public static async Task<string> GetStreamMd5Async(Stream stream, CancellationToken token = default)
{
using (MD5 md5 = MD5.Create())
{
byte[] bytes = await md5.ComputeHashAsync(stream, token).ConfigureAwait(false);
return System.Convert.ToHexString(bytes);
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 文件操作
/// </summary>
internal static class FileOperation
{
/// <summary>
/// 将指定文件移动到新位置,提供指定新文件名和覆盖目标文件(如果它已存在)的选项。
/// </summary>
/// <param name="sourceFileName">要移动的文件的名称。 可以包括相对或绝对路径。</param>
/// <param name="destFileName">文件的新路径和名称。</param>
/// <param name="overwrite">如果要覆盖目标文件</param>
/// <returns>是否发生了移动操作</returns>
public static bool Move(string sourceFileName, string destFileName, bool overwrite)
{
if (File.Exists(sourceFileName))
{
if (overwrite)
{
File.Move(sourceFileName, destFileName, overwrite);
return true;
}
else
{
if (!File.Exists(destFileName))
{
File.Move(sourceFileName, destFileName, overwrite);
return true;
}
}
}
return false;
}
}

View File

@@ -109,4 +109,4 @@ internal readonly struct FilePath : IEquatable<FilePath>
{
return Value.GetHashCode();
}
}
}

View File

@@ -36,7 +36,7 @@ internal static class PickerExtension
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning($"无法打开文件选择器 {exception.Message}");
.Warning("无法打开文件选择器", $"请勿在管理员模式下使用此功能 {exception.Message}");
}
return new(false, null!);
@@ -68,7 +68,7 @@ internal static class PickerExtension
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning($"无法打开文件选择器 {exception.Message}");
.Warning("无法打开文件选择器", $"请勿在管理员模式下使用此功能 {exception.Message}");
}
return new(false, null!);

View File

@@ -53,6 +53,12 @@ internal sealed class TempFile : IDisposable
/// </summary>
public void Dispose()
{
File.Delete(Path);
try
{
File.Delete(Path);
}
catch (IOException)
{
}
}
}

View File

@@ -11,8 +11,8 @@ internal class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEnumerabl
/// <inheritdoc/>
public override IEnumerable<int> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? team = reader.GetString();
IEnumerable<int>? ids = team?.Split(',').Select(x => int.Parse(x));
string? source = reader.GetString();
IEnumerable<int>? ids = source?.Split(',').Select(int.Parse);
return ids ?? Enumerable.Empty<int>();
}

View File

@@ -16,7 +16,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
return false;
}
if (typeToConvert.GetGenericTypeDefinition() != typeof(IDictionary<,>))
if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
{
return false;
}
@@ -27,8 +27,9 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
/// <inheritdoc/>
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
Type keyType = type.GetGenericArguments()[0];
Type valueType = type.GetGenericArguments()[1];
Type[] arguments = type.GetGenericArguments();
Type keyType = arguments[0];
Type valueType = arguments[1];
Type innerConverterType = typeof(StringEnumDictionaryConverterInner<,>).MakeGenericType(keyType, valueType);
JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType)!;

View File

@@ -10,6 +10,8 @@ namespace Snap.Hutao.Core.Json;
/// </summary>
internal class JsonTextEncoder : JavaScriptEncoder
{
private static readonly string BackSlashDoubleQuote = "\\\"";
/// <inheritdoc/>
public override int MaxOutputCharactersPerInputCharacter { get => 6; }
@@ -27,7 +29,7 @@ internal class JsonTextEncoder : JavaScriptEncoder
if (unicodeScalar == '"')
{
numberOfCharactersWritten = 2;
return "\\\"".AsSpan().TryCopyTo(new Span<char>(buffer, bufferLength));
return BackSlashDoubleQuote.AsSpan().TryCopyTo(new Span<char>(buffer, bufferLength));
}
string encoded = $"\\u{(uint)unicodeScalar:x4}";

View File

@@ -10,7 +10,9 @@ using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Navigation;
#if RELEASE
using System.Security.Principal;
#endif
namespace Snap.Hutao.Core.LifeCycle;
@@ -37,11 +39,15 @@ internal static class Activation
/// <returns>是否提升了权限</returns>
public static bool GetElevated()
{
#if RELEASE
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
#else
return true;
#endif
}
/// <summary>
@@ -149,7 +155,7 @@ internal static class Activation
Ioc.Default
.GetRequiredService<IMetadataService>()
.ImplictAs<IMetadataInitializer>()?
.ImplictAs<IMetadataServiceInitialization>()?
.InitializeInternalAsync()
.SafeForget();
}

View File

@@ -23,6 +23,7 @@ internal static class ScheduleTaskHelper
{
try
{
// TODO: 似乎可以不删除任务,直接注册已经包含了更新功能
SchedulerTask? targetTask = TaskService.Instance.GetTask(DailyNoteRefreshTaskName);
if (targetTask != null)
{
@@ -36,12 +37,9 @@ internal static class ScheduleTaskHelper
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);
return true;
}
catch (UnauthorizedAccessException)
{
return false;
}
catch (COMException)
catch (Exception ex)
{
_ = ex;
return false;
}
}

View File

@@ -24,7 +24,10 @@ internal static class SettingKeys
public const string LaunchTimes = "LaunchTimes";
/// <summary>
/// 静态资源合约V1
/// 静态资源合约
/// 新增合约时 请注意
/// <see cref="StaticResource.FulfillAllContracts"/>
/// 与 <see cref="StaticResource.IsAnyUnfulfilledContractPresent"/>
/// </summary>
public const string StaticResourceV1Contract = "StaticResourceV1Contract";
@@ -32,4 +35,14 @@ internal static class SettingKeys
/// 静态资源合约V2 成就图标与物品图标
/// </summary>
public const string StaticResourceV2Contract = "StaticResourceV2Contract";
/// <summary>
/// 静态资源合约V3 刷新 Skill Talent
/// </summary>
public const string StaticResourceV3Contract = "StaticResourceV3Contract";
/// <summary>
/// 静态资源合约V4 刷新 AvatarIcon
/// </summary>
public const string StaticResourceV4Contract = "StaticResourceV4Contract";
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Setting;
/// <summary>
/// 静态资源
/// </summary>
internal static class StaticResource
{
/// <summary>
/// 完成所有合约
/// </summary>
public static void FulfillAllContracts()
{
LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV3Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV4Contract, true);
}
/// <summary>
/// 提供的合约是否未完成
/// </summary>
/// <param name="contractKey">合约的键</param>
/// <returns>合约是否未完成</returns>
public static bool IsContractUnfulfilled(string contractKey)
{
return !LocalSetting.Get(contractKey, false);
}
/// <summary>
/// 是否有任何静态资源合约尚未完成
/// </summary>
/// <returns>静态资源合约尚未完成</returns>
public static bool IsAnyUnfulfilledContractPresent()
{
return !LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false)
|| (!LocalSetting.Get(SettingKeys.StaticResourceV2Contract, false))
|| (!LocalSetting.Get(SettingKeys.StaticResourceV3Contract, false))
|| (!LocalSetting.Get(SettingKeys.StaticResourceV4Contract, false));
}
}

View File

@@ -1,46 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// Holds the task for a cancellation token, as well as the token registration. The registration is disposed when this instance is disposed.
/// </summary>
/// <typeparam name="T">包装类型</typeparam>
public sealed class CancellationTokenTaskCompletionSource : IDisposable
{
/// <summary>
/// The cancellation token registration, if any. This is <c>null</c> if the registration was not necessary.
/// </summary>
private readonly IDisposable? registration;
/// <summary>
/// Creates a task for the specified cancellation token, registering with the token if necessary.
/// </summary>
/// <param name="cancellationToken">The cancellation token to observe.</param>
public CancellationTokenTaskCompletionSource(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
Task = Task.CompletedTask;
return;
}
TaskCompletionSource tcs = new();
registration = cancellationToken.Register(() => tcs.TrySetResult(), useSynchronizationContext: false);
Task = tcs.Task;
}
/// <summary>
/// Gets the task for the source cancellation token.
/// </summary>
public Task Task { get; private set; }
/// <summary>
/// Disposes the cancellation token registration, if any. Note that this may cause <see cref="Task"/> to never complete.
/// </summary>
public void Dispose()
{
registration?.Dispose();
}
}

View File

@@ -6,9 +6,30 @@ using System.Collections.Concurrent;
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 并发<see cref="CancellationTokenSource"/>
/// 无区分项的并发<see cref="CancellationTokenSource"/>
/// </summary>
[SuppressMessage("", "CA1001")]
internal class ConcurrentCancellationTokenSource
{
private CancellationTokenSource source = new();
/// <summary>
/// 注册取消令牌
/// </summary>
/// <returns>取消令牌</returns>
public CancellationToken Register()
{
source.Cancel();
source = new();
return source.Token;
}
}
/// <summary>
/// 有区分项的并发<see cref="CancellationTokenSource"/>
/// </summary>
/// <typeparam name="TItem">项类型</typeparam>
[SuppressMessage("", "SA1402")]
internal class ConcurrentCancellationTokenSource<TItem>
where TItem : notnull
{
@@ -17,7 +38,7 @@ internal class ConcurrentCancellationTokenSource<TItem>
/// <summary>
/// 为某个项注册取消令牌
/// </summary>
/// <param name="item">项</param>
/// <param name="item">区分项</param>
/// <returns>取消令牌</returns>
public CancellationToken Register(TItem item)
{
@@ -28,4 +49,4 @@ internal class ConcurrentCancellationTokenSource<TItem>
return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token;
}
}
}

View File

@@ -17,15 +17,13 @@ public static class DispatcherQueueExtension
/// <param name="action">执行的回调</param>
public static void Invoke(this DispatcherQueue dispatcherQueue, Action action)
{
using (ManualResetEventSlim blockEvent = new())
ManualResetEventSlim blockEvent = new();
dispatcherQueue.TryEnqueue(() =>
{
dispatcherQueue.TryEnqueue(() =>
{
action();
blockEvent.Set();
});
action();
blockEvent.Set();
});
blockEvent.Wait();
}
blockEvent.Wait();
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Dispatching;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Threading;
@@ -10,6 +11,21 @@ namespace Snap.Hutao.Core.Threading;
/// </summary>
internal static class ThreadHelper
{
/// <summary>
/// 主线程队列
/// </summary>
private static volatile DispatcherQueue? dispatcherQueue;
/// <summary>
/// 初始化
/// </summary>
public static void Initialize()
{
dispatcherQueue = DispatcherQueue.GetForCurrentThread();
DispatcherQueueSynchronizationContext context = new(dispatcherQueue);
SynchronizationContext.SetSynchronizationContext(context);
}
/// <summary>
/// 使用此静态方法以 异步切换到 后台线程
/// </summary>
@@ -29,7 +45,7 @@ internal static class ThreadHelper
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static DispatherQueueSwitchOperation SwitchToMainThreadAsync()
{
return new(Program.DispatcherQueue!);
return new(dispatcherQueue!);
}
/// <summary>
@@ -39,6 +55,13 @@ internal static class ThreadHelper
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void InvokeOnMainThread(Action action)
{
Program.DispatcherQueue!.Invoke(action);
if (dispatcherQueue!.HasThreadAccess)
{
action();
}
else
{
dispatcherQueue.Invoke(action);
}
}
}

View File

@@ -11,7 +11,7 @@ public enum BackdropType
/// <summary>
/// 无
/// </summary>
None = 0,
None,
/// <summary>
/// 亚克力

View File

@@ -148,6 +148,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
IMessenger messenger = Ioc.Default.GetRequiredService<IMessenger>();
messenger.Register<BackdropTypeChangedMessage>(this);
messenger.Register<FlyoutOpenCloseMessage>(this);
window.Closed += OnWindowClosed;
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="BinaryReader"/> 扩展
/// </summary>
public static class BinaryReaderExtension
{
/// <summary>
/// 判断是否处于流的结尾
/// </summary>
/// <param name="reader">读取器</param>
/// <returns>是否处于流的结尾</returns>
public static bool EndOfStream(this BinaryReader reader)
{
return reader.BaseStream.Position >= reader.BaseStream.Length;
}
}

View File

@@ -18,7 +18,7 @@ public static class DateTimeOffsetExtension
{
if (keepTicks)
{
dateTimeOffset += TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now).Negate();
dateTimeOffset -= TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now);
}
return dateTimeOffset.ToLocalTime();

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
@@ -37,6 +38,7 @@ public static partial class EnumerableExtension
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>源列表或空列表</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static List<TSource> EmptyIfNull<TSource>(this List<TSource>? source)
{
return source ?? new();

View File

@@ -9,13 +9,6 @@ namespace Snap.Hutao.Extension;
[SuppressMessage("", "CA2254")]
public static class LoggerExtension
{
/// <inheritdoc cref="LoggerExtensions.LogInformation(ILogger, string?, object?[])"/>
public static T LogInformation<T>(this ILogger logger, string message, params object?[] param)
{
logger.LogInformation(message, param);
return default!;
}
/// <inheritdoc cref="LoggerExtensions.LogWarning(ILogger, string?, object?[])"/>
public static T LogWarning<T>(this ILogger logger, string message, params object?[] param)
{

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Extension;
/// <summary>
@@ -8,36 +10,15 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static class NumberExtension
{
/// <summary>
/// 获取从右向左某位上的数字
/// </summary>
/// <param name="x">源</param>
/// <param name="place">位</param>
/// <returns>数字</returns>
public static int AtPlace(this int x, int place)
{
return (int)(x / Math.Pow(10, place - 1)) % 10;
}
/// <summary>
/// 计算给定整数的位数
/// </summary>
/// <param name="x">给定的整数</param>
/// <returns>位数</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Place(this int x)
{
// Benchmarked and compared as a most optimized solution
return (int)(MathF.Log10(x) + 1);
}
/// <summary>
/// 计算给定整数的位数
/// </summary>
/// <param name="x">给定的整数</param>
/// <returns>位数</returns>
public static int Place(this long x)
{
// Benchmarked and compared as a most optimized solution
return (int)(MathF.Log10(x) + 1);
}
}

View File

@@ -1,76 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Input;
namespace Snap.Hutao.Factory.Abstraction;
/// <summary>
/// Factory for creating <see cref="AsyncRelayCommand"/> with additional processing.
/// </summary>
public interface IAsyncRelayCommandFactory
{
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<Task> execute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<Task> execute, Func<bool> canExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute, Predicate<T?> canExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="execute">The execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute, Predicate<T?> canExecute);
}

View File

@@ -1,102 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Factory.Abstraction;
namespace Snap.Hutao.Factory;
/// <inheritdoc cref="IAsyncRelayCommandFactory"/>
[Injection(InjectAs.Transient, typeof(IAsyncRelayCommandFactory))]
internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
private readonly ILogger<AsyncRelayCommandFactory> logger;
/// <summary>
/// 构造一个新的异步命令工厂
/// </summary>
/// <param name="logger">日志器</param>
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger)
{
this.logger = logger;
}
/// <inheritdoc/>
public AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute)
{
return Register(new AsyncRelayCommand<T>(execute));
}
/// <inheritdoc/>
public AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute)
{
return Register(new AsyncRelayCommand<T>(cancelableExecute));
}
/// <inheritdoc/>
public AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute, Predicate<T?> canExecute)
{
return Register(new AsyncRelayCommand<T>(execute, canExecute));
}
/// <inheritdoc/>
public AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute, Predicate<T?> canExecute)
{
return Register(new AsyncRelayCommand<T>(cancelableExecute, canExecute));
}
/// <inheritdoc/>
public AsyncRelayCommand Create(Func<Task> execute)
{
return Register(new AsyncRelayCommand(execute));
}
/// <inheritdoc/>
public AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute)
{
return Register(new AsyncRelayCommand(cancelableExecute));
}
/// <inheritdoc/>
public AsyncRelayCommand Create(Func<Task> execute, Func<bool> canExecute)
{
return Register(new AsyncRelayCommand(execute, canExecute));
}
/// <inheritdoc/>
public AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute)
{
return Register(new AsyncRelayCommand(cancelableExecute, canExecute));
}
private AsyncRelayCommand Register(AsyncRelayCommand command)
{
ReportException(command);
return command;
}
private AsyncRelayCommand<T> Register<T>(AsyncRelayCommand<T> command)
{
ReportException(command);
return command;
}
private void ReportException(IAsyncRelayCommand command)
{
command.PropertyChanged += (sender, args) =>
{
if (sender is IAsyncRelayCommand asyncRelayCommand)
{
if (args.PropertyName == nameof(AsyncRelayCommand.ExecutionTask))
{
if (asyncRelayCommand.ExecutionTask?.Exception is AggregateException exception)
{
Exception baseException = exception.GetBaseException();
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(AsyncRelayCommand));
}
}
}
};
}
}

View File

@@ -33,11 +33,8 @@ public sealed partial class MainWindow : Window, IExtendedWindowSource, IRecipie
Ioc.Default.GetRequiredService<IMessenger>().Register(this);
// Query the StaticResourceV1Contract & StaticResourceV2Contract.
// If not complete we should present the welcome view.
ContentSwitchPresenter.Value =
!LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false)
|| (!LocalSetting.Get(SettingKeys.StaticResourceV2Contract, false));
ContentSwitchPresenter.Value = StaticResource.IsAnyUnfulfilledContractPresent();
}
/// <summary>

View File

@@ -4,7 +4,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Service.Cultivation;
namespace Snap.Hutao.Model.Binding.Cultivation;
@@ -25,7 +24,7 @@ public class CultivateItem : ObservableObject
Inner = inner;
Entity = entity;
isFinished = Entity.IsFinished;
IsToday = CultivateItemHelper.IsTodaysMaterial(inner.Id, DateTimeOffset.Now);
IsToday = inner.IsTodaysItem();
FinishStateCommand = new RelayCommand(FlipIsFinished);
}
@@ -55,7 +54,6 @@ public class CultivateItem : ObservableObject
if (SetProperty(ref isFinished, value))
{
Entity.IsFinished = value;
Ioc.Default.GetRequiredService<ICultivationService>().SaveCultivateItem(Entity);
}
}
}

View File

@@ -1,64 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.Cultivation;
/// <summary>
/// 养成物品帮助类
/// </summary>
public static class CultivateItemHelper
{
/// <summary>
/// 判断是否为当日物品
/// </summary>
/// <param name="itemId">材料Id</param>
/// <param name="now">时间</param>
/// <returns>是否为当日物品</returns>
public static bool IsTodaysMaterial(int itemId, DateTimeOffset now)
{
DateTimeOffset utcNow = now.ToUniversalTime();
utcNow = utcNow.AddHours(4);
DayOfWeek dayOfWeek = utcNow.DayOfWeek;
return dayOfWeek switch
{
DayOfWeek.Monday or DayOfWeek.Thursday => itemId switch
{
104301 or 104302 or 104303 => true, // 「自由」
104310 or 104311 or 104312 => true, // 「繁荣」
104320 or 104321 or 104322 => true, // 「浮世」
104329 or 104330 or 104331 => true, // 「诤言」
114001 or 114002 or 114003 or 114004 => true, // 高塔孤王
114013 or 114014 or 114015 or 114016 => true, // 孤云寒林
114025 or 114026 or 114027 or 114028 => true, // 远海夷地
114037 or 114038 or 114039 or 114040 => true, // 谧林涓露
_ => false,
},
DayOfWeek.Tuesday or DayOfWeek.Friday => itemId switch
{
104304 or 104305 or 104306 => true, // 「抗争」
104313 or 104314 or 104315 => true, // 「勤劳」
104323 or 104324 or 104325 => true, // 「风雅」
104332 or 104333 or 104334 => true, // 「巧思」
114005 or 114006 or 114007 or 114008 => true, // 凛风奔狼
114017 or 114018 or 114019 or 114020 => true, // 雾海云间
114029 or 114030 or 114031 or 114032 => true, // 鸣神御灵
114041 or 114042 or 114043 or 114044 => true, // 绿洲花园
_ => false,
},
DayOfWeek.Wednesday or DayOfWeek.Saturday => itemId switch
{
104307 or 104308 or 104309 => true, // 「诗文」
104316 or 104317 or 104318 => true, // 「黄金」
104326 or 104327 or 104328 => true, // 「天光」
104335 or 104336 or 104337 => true, // 「笃行」
114009 or 114010 or 114011 or 114012 => true, // 狮牙斗士
114021 or 114022 or 114023 or 114024 => true, // 漆黑陨铁
114033 or 114034 or 114035 or 114036 => true, // 今昔剧画
114045 or 114046 or 114047 or 114048 => true, // 谧林涓露
_ => false,
},
_ => false,
};
}
}

View File

@@ -1,36 +1,45 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Immutable;
namespace Snap.Hutao.Model.Binding.LaunchGame;
/// <summary>
/// 服务器方案
/// </summary>
/// <summary>
/// 启动方案
/// </summary>
public class LaunchScheme
{
/// <summary>
/// 已知的启动方案
/// </summary>
public static readonly ImmutableList<LaunchScheme> KnownSchemes = new List<LaunchScheme>()
{
new LaunchScheme("官方服 | 天空岛", "eYd89JmJ", "18", "1", "1"),
new LaunchScheme("渠道服 | 世界树", "KAtdSsoQ", "17", "14", "0"),
new LaunchScheme("国际服 | 部分支持", "gcStgarh", "10", "1", "0"),
}.ToImmutableList();
/// <summary>
/// 构造一个新的启动方案
/// </summary>
/// <param name="name">名称</param>
/// <param name="displayName">名称</param>
/// <param name="channel">通道</param>
/// <param name="cps">通道描述字符串</param>
/// <param name="subChannel">子通道</param>
/// <param name="launcherId">启动器Id</param>
public LaunchScheme(string name, string channel, string subChannel, string launcherId)
private LaunchScheme(string displayName, string key, string launcherId, string channel, string subChannel)
{
Name = name;
DisplayName = displayName;
Channel = channel;
SubChannel = subChannel;
LauncherId = launcherId;
Key = key;
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
public string DisplayName { get; set; }
/// <summary>
/// 通道
@@ -46,4 +55,14 @@ public class LaunchScheme
/// 启动器Id
/// </summary>
public string LauncherId { get; set; }
/// <summary>
/// API Key
/// </summary>
public string Key { get; set; }
/// <summary>
/// 是否为海外
/// </summary>
public bool IsOversea { get => LauncherId == "10"; }
}

View File

@@ -22,12 +22,14 @@ public class UIAF
/// <summary>
/// 信息
/// </summary>
[JsonPropertyName("info")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public UIAFInfo Info { get; set; } = default!;
/// <summary>
/// 列表
/// </summary>
[JsonPropertyName("list")]
public List<UIAFItem> List { get; set; } = default!;
/// <summary>

View File

@@ -13,21 +13,25 @@ public class UIAFItem
/// <summary>
/// 成就Id
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 完成时间
/// </summary>
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; }
/// <summary>
/// 当前值
/// 对于progress为1的项该属性始终为0
/// </summary>
[JsonPropertyName("current")]
public int Current { get; set; }
/// <summary>
/// 完成状态
/// </summary>
[JsonPropertyName("status")]
public AchievementInfoStatus Status { get; set; }
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Intrinsic;
/// <summary>
/// 不可变的原生枚举
/// </summary>
public static class ImmutableIntrinsics
public static class IntrinsicImmutables
{
/// <summary>
/// 所属地区

View File

@@ -13,47 +13,47 @@ public enum MaterialType
MATERIAL_FOOD = 1,
MATERIAL_QUEST = 2,
MATERIAL_EXCHANGE = 4,
MATERIAL_CONSUME,
MATERIAL_EXP_FRUIT,
MATERIAL_AVATAR,
MATERIAL_ADSORBATE,
MATERIAL_CRICKET,
MATERIAL_ELEM_CRYSTAL,
MATERIAL_WEAPON_EXP_STONE,
MATERIAL_CHEST,
MATERIAL_RELIQUARY_MATERIAL,
MATERIAL_AVATAR_MATERIAL,
MATERIAL_NOTICE_ADD_HP,
MATERIAL_SEA_LAMP,
MATERIAL_SELECTABLE_CHEST,
MATERIAL_FLYCLOAK,
MATERIAL_NAMECARD,
MATERIAL_TALENT,
MATERIAL_WIDGET,
MATERIAL_CHEST_BATCH_USE,
MATERIAL_FAKE_ABSORBATE,
MATERIAL_CONSUME_BATCH_USE,
MATERIAL_WOOD,
MATERIAL_CONSUME = 5,
MATERIAL_EXP_FRUIT = 6,
MATERIAL_AVATAR = 7,
MATERIAL_ADSORBATE = 8,
MATERIAL_CRICKET = 9,
MATERIAL_ELEM_CRYSTAL = 10,
MATERIAL_WEAPON_EXP_STONE = 11,
MATERIAL_CHEST = 12,
MATERIAL_RELIQUARY_MATERIAL = 13,
MATERIAL_AVATAR_MATERIAL = 14,
MATERIAL_NOTICE_ADD_HP = 15,
MATERIAL_SEA_LAMP = 16,
MATERIAL_SELECTABLE_CHEST = 17,
MATERIAL_FLYCLOAK = 18,
MATERIAL_NAMECARD = 19,
MATERIAL_TALENT = 20,
MATERIAL_WIDGET = 21,
MATERIAL_CHEST_BATCH_USE = 22,
MATERIAL_FAKE_ABSORBATE = 23,
MATERIAL_CONSUME_BATCH_USE = 24,
MATERIAL_WOOD = 25,
MATERIAL_FURNITURE_FORMULA = 27,
MATERIAL_CHANNELLER_SLAB_BUFF,
MATERIAL_FURNITURE_SUITE_FORMULA,
MATERIAL_COSTUME,
MATERIAL_HOME_SEED,
MATERIAL_FISH_BAIT,
MATERIAL_FISH_ROD,
MATERIAL_SUMO_BUFF, // never appear
MATERIAL_FIREWORKS,
MATERIAL_BGM,
MATERIAL_SPICE_FOOD,
MATERIAL_ACTIVITY_ROBOT,
MATERIAL_ACTIVITY_GEAR,
MATERIAL_ACTIVITY_JIGSAW,
MATERIAL_ARANARA,
MATERIAL_GCG_CARD,
MATERIAL_GCG_CARD_FACE, // 影幻卡面
MATERIAL_GCG_CARD_BACK,
MATERIAL_GCG_FIELD,
MATERIAL_DESHRET_MANUAL,
MATERIAL_RENAME_ITEM,
MATERIAL_GCG_EXCHANGE_ITEM,
MATERIAL_CHANNELLER_SLAB_BUFF = 28,
MATERIAL_FURNITURE_SUITE_FORMULA = 29,
MATERIAL_COSTUME = 30,
MATERIAL_HOME_SEED = 31,
MATERIAL_FISH_BAIT = 32,
MATERIAL_FISH_ROD = 33,
MATERIAL_SUMO_BUFF = 34, // never appear
MATERIAL_FIREWORKS = 35,
MATERIAL_BGM = 36,
MATERIAL_SPICE_FOOD = 37,
MATERIAL_ACTIVITY_ROBOT = 38,
MATERIAL_ACTIVITY_GEAR = 39,
MATERIAL_ACTIVITY_JIGSAW = 40,
MATERIAL_ARANARA = 41,
MATERIAL_GCG_CARD = 42,
MATERIAL_GCG_CARD_FACE = 43, // 影幻卡面
MATERIAL_GCG_CARD_BACK = 44,
MATERIAL_GCG_FIELD = 45,
MATERIAL_DESHRET_MANUAL = 46,
MATERIAL_RENAME_ITEM = 47,
MATERIAL_GCG_EXCHANGE_ITEM = 48,
}

View File

@@ -31,10 +31,10 @@ public class Costume
/// <summary>
/// 图标
/// </summary>
public string? Icon { get; set; }
public string Icon { get; set; } = default!;
/// <summary>
/// 侧面图标
/// </summary>
public string? SideIcon { get; set; }
public string SideIcon { get; set; } = default!;
}

View File

@@ -97,8 +97,20 @@ public static class AvatarIds
{
return new(idAvatarMap)
{
[PlayerBoy] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Intrinsic.ItemQuality.QUALITY_ORANGE },
[PlayerGirl] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Intrinsic.ItemQuality.QUALITY_ORANGE },
[PlayerBoy] = new()
{
Name = "旅行者",
Icon = "UI_AvatarIcon_PlayerBoy",
SideIcon = "UI_AvatarIcon_Side_PlayerBoy",
Quality = Intrinsic.ItemQuality.QUALITY_ORANGE,
},
[PlayerGirl] = new()
{
Name = "旅行者",
Icon = "UI_AvatarIcon_PlayerGirl",
SideIcon = "UI_AvatarIcon_Side_PlayerGirl",
Quality = Intrinsic.ItemQuality.QUALITY_ORANGE,
},
};
}
}

View File

@@ -34,6 +34,8 @@ internal class ElementNameIconConverter : ValueConverter<string, Uri>
return string.IsNullOrEmpty(element)
? Web.HutaoEndpoints.UIIconNone
: new Uri(Web.HutaoEndpoints.StaticFile("IconElement", $"UI_Icon_Element_{element}.png"));
// $"UI_Icon_Element_{element}.png"
}
/// <summary>

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Primitive;
using System.Collections.Immutable;
namespace Snap.Hutao.Model.Metadata;
@@ -11,6 +12,42 @@ namespace Snap.Hutao.Model.Metadata;
/// </summary>
public class Material
{
private static readonly ImmutableHashSet<MaterialId> MondayThursdayItems = new HashSet<MaterialId>
{
104301, 104302, 104303, // 「自由」
104310, 104311, 104312, // 「繁荣」
104320, 104321, 104322, // 「浮世」
104329, 104330, 104331, // 「诤言」
114001, 114002, 114003, 114004, // 高塔孤王
114013, 114014, 114015, 114016, // 孤云寒林
114025, 114026, 114027, 114028, // 远海夷地
114037, 114038, 114039, 114040, // 谧林涓露
}.ToImmutableHashSet();
private static readonly ImmutableHashSet<MaterialId> TuesdayFridayItems = new HashSet<MaterialId>
{
104304, 104305, 104306, // 「抗争」
104313, 104314, 104315, // 「勤劳」
104323, 104324, 104325, // 「风雅」
104332, 104333, 104334, // 「巧思」
114005, 114006, 114007, 114008, // 凛风奔狼
114017, 114018, 114019, 114020, // 雾海云间
114029, 114030, 114031, 114032, // 鸣神御灵
114041, 114042, 114043, 114044, // 绿洲花园
}.ToImmutableHashSet();
private static readonly ImmutableHashSet<MaterialId> WednesdaySaturdayItems = new HashSet<MaterialId>
{
104307, 104308, 104309, // 「诗文」
104316, 104317, 104318, // 「黄金」
104326, 104327, 104328, // 「天光」
104335, 104336, 104337, // 「笃行」
114009, 114010, 114011, 114012, // 狮牙斗士
114021, 114022, 114023, 114024, // 漆黑陨铁
114033, 114034, 114035, 114036, // 今昔剧画
114045, 114046, 114047, 114048, // 谧林涓露
}.ToImmutableHashSet();
/// <summary>
/// 物品Id
/// </summary>
@@ -89,4 +126,21 @@ public class Material
_ => false,
};
}
/// <summary>
/// 判断是否为当日物品
/// O(1) 操作
/// </summary>
/// <param name="treatSundayAsTrue">星期日视为当日材料</param>
/// <returns>是否为当日物品</returns>
public bool IsTodaysItem(bool treatSundayAsTrue = false)
{
return DateTimeOffset.UtcNow.AddHours(4).DayOfWeek switch
{
DayOfWeek.Monday or DayOfWeek.Thursday => MondayThursdayItems.Contains(Id),
DayOfWeek.Tuesday or DayOfWeek.Friday => TuesdayFridayItems.Contains(Id),
DayOfWeek.Wednesday or DayOfWeek.Saturday => WednesdaySaturdayItems.Contains(Id),
_ => false,
};
}
}

View File

@@ -25,5 +25,5 @@ public class ReliquaryLevel
/// 属性
/// </summary>
[JsonConverter(typeof(StringEnumKeyDictionaryConverter))]
public IDictionary<FightProperty, double> Properties { get; set; } = default!;
public Dictionary<FightProperty, double> Properties { get; set; } = default!;
}

View File

@@ -12,7 +12,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.3.9.0" />
Version="1.4.0.0" />
<Properties>
<DisplayName>胡桃</DisplayName>
@@ -24,7 +24,7 @@
<Dependencies>
<!--<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />-->
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.18362.0" MaxVersionTested="10.0.22000.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Logging;
@@ -17,14 +16,6 @@ namespace Snap.Hutao;
/// </summary>
public static partial class Program
{
/// <summary>
/// 主线程队列
/// </summary>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[SuppressMessage("", "SA1401")]
internal static volatile DispatcherQueue? DispatcherQueue;
[LibraryImport("Microsoft.ui.xaml.dll")]
private static partial void XamlCheckProcessRequirements();
@@ -50,10 +41,7 @@ public static partial class Program
private static void InitializeApp(ApplicationInitializationCallbackParams param)
{
DispatcherQueue = DispatcherQueue.GetForCurrentThread();
DispatcherQueueSynchronizationContext context = new(DispatcherQueue);
SynchronizationContext.SetSynchronizationContext(context);
ThreadHelper.Initialize();
_ = Ioc.Default.GetRequiredService<App>();
}

View File

@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity.Database;
@@ -71,8 +72,7 @@ internal class AchievementService : IAchievementService
// Cascade deleted the achievements.
await appDbContext.AchievementArchives
.Where(a => a.InnerId == archive.InnerId)
.ExecuteDeleteAsync()
.ExecuteDeleteWhereAsync(a => a.InnerId == archive.InnerId)
.ConfigureAwait(false);
}
@@ -114,7 +114,17 @@ internal class AchievementService : IAchievementService
List<BindingAchievement> results = new();
foreach (MetadataAchievement meta in metadata)
{
EntityAchievement entity = entities.SingleOrDefault(e => e.Id == meta.Id) ?? EntityAchievement.Create(archiveId, meta.Id);
EntityAchievement? entity;
try
{
entity = entities.SingleOrDefault(e => e.Id == meta.Id);
}
catch (InvalidOperationException ex)
{
throw new UserdataCorruptedException("单个成就存档内发现多个相同的成就 Id", ex);
}
entity ??= EntityAchievement.Create(archiveId, meta.Id);
results.Add(new(meta, entity));
}

View File

@@ -22,7 +22,6 @@ namespace Snap.Hutao.Service.AvatarInfo;
[Injection(InjectAs.Scoped, typeof(IAvatarInfoService))]
internal class AvatarInfoService : IAvatarInfoService
{
private readonly AppDbContext appDbContext;
private readonly ISummaryFactory summaryFactory;
private readonly IMetadataService metadataService;
private readonly ILogger<AvatarInfoService> logger;
@@ -42,7 +41,6 @@ internal class AvatarInfoService : IAvatarInfoService
ISummaryFactory summaryFactory,
ILogger<AvatarInfoService> logger)
{
this.appDbContext = appDbContext;
this.metadataService = metadataService;
this.summaryFactory = summaryFactory;
this.logger = logger;

View File

@@ -6,7 +6,7 @@ using Snap.Hutao.Model.Metadata;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 权重配置
/// 圣遗物评分权重配置
/// </summary>
internal static partial class ReliquaryWeightConfiguration
{

View File

@@ -43,26 +43,53 @@ internal class SummaryAvatarFactory
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId];
return new()
PropertyAvatar propertyAvatar = new()
{
// metadata part
Id = avatar.Id,
Name = avatar.Name,
Icon = AvatarIconConverter.IconNameToUri(avatar.Icon),
SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon),
NameCard = AvatarNameCardPicConverter.AvatarToUri(avatar),
Quality = avatar.Quality,
NameCard = AvatarNameCardPicConverter.AvatarToUri(avatar),
Element = ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore),
Level = $"Lv.{avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value}",
LevelNumber = int.Parse(avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value ?? string.Empty),
FetterLevel = avatarInfo.FetterInfo.ExpLevel,
Weapon = reliquaryAndWeapon.Weapon,
Reliquaries = reliquaryAndWeapon.Reliquaries,
// webinfo & metadata mixed part
Constellations = SummaryHelper.CreateConstellations(avatarInfo.TalentIdList, avatar.SkillDepot.Talents),
Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.GetCompositeSkillsNoInherents()),
// webinfo part
FetterLevel = avatarInfo.FetterInfo?.ExpLevel ?? 0,
Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap),
Score = reliquaryAndWeapon.Reliquaries.Sum(r => r.Score).ToString("F2"),
CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}",
LevelNumber = avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].ValueInt32 ?? 0,
// processed webinfo part
Weapon = reliquaryAndWeapon.Weapon,
Reliquaries = reliquaryAndWeapon.Reliquaries,
Score = reliquaryAndWeapon.Reliquaries.Sum(r => r.Score).ToString("F2"),
};
TryApplyCostumeIconToAvatar(ref propertyAvatar, avatar);
propertyAvatar.Level = $"Lv.{propertyAvatar.LevelNumber}";
return propertyAvatar;
}
private void TryApplyCostumeIconToAvatar(ref PropertyAvatar propertyAvatar, MetadataAvatar avatar)
{
if (avatarInfo.CostumeId.HasValue)
{
int costumeId = avatarInfo.CostumeId.Value;
Model.Metadata.Avatar.Costume costume = avatar.Costumes.Single(c => c.Id == costumeId);
// Set to costume icon
propertyAvatar.Icon = AvatarIconConverter.IconNameToUri(costume.Icon);
propertyAvatar.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon);
}
else
{
propertyAvatar.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
propertyAvatar.SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon);
}
}
private ReliquaryAndWeapon ProcessEquip(List<Equip> equipments)
@@ -85,7 +112,7 @@ internal class SummaryAvatarFactory
}
}
return new(reliquaryList, weapon!);
return new(reliquaryList, weapon);
}
private PropertyWeapon CreateWeapon(Equip equip)
@@ -143,4 +170,4 @@ internal class SummaryAvatarFactory
Weapon = weapon;
}
}
}
}

View File

@@ -5,13 +5,16 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Metadata;
using System.Collections.ObjectModel;
using BindingCultivateEntry = Snap.Hutao.Model.Binding.Cultivation.CultivateEntry;
using BindingCultivateItem = Snap.Hutao.Model.Binding.Cultivation.CultivateItem;
using BindingInventoryItem = Snap.Hutao.Model.Binding.Inventory.InventoryItem;
using BindingStatisticsItem = Snap.Hutao.Model.Binding.Cultivation.StatisticsCultivateItem;
namespace Snap.Hutao.Service.Cultivation;
@@ -102,7 +105,10 @@ internal class CultivationService : ICultivationService
await ThreadHelper.SwitchToBackgroundAsync();
using (IServiceScope scope = scopeFactory.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<AppDbContext>().CultivateProjects.RemoveAndSaveAsync(project).ConfigureAwait(false);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateProjects
.ExecuteDeleteWhereAsync(p => p.InnerId == project.InnerId)
.ConfigureAwait(false);
}
}
@@ -129,22 +135,21 @@ internal class CultivationService : ICultivationService
}
/// <inheritdoc/>
public async Task<ObservableCollection<BindingCultivateEntry>> GetCultivateEntriesAsync(
CultivateProject cultivateProject,
List<Model.Metadata.Material> materials,
Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap,
Dictionary<WeaponId, Model.Metadata.Weapon.Weapon> idWeaponMap)
public async Task<ObservableCollection<BindingCultivateEntry>> GetCultivateEntriesAsync(CultivateProject cultivateProject)
{
await ThreadHelper.SwitchToBackgroundAsync();
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IMetadataService metadataService = scope.ServiceProvider.GetRequiredService<IMetadataService>();
Guid projectId = cultivateProject.InnerId;
List<Model.Metadata.Material> materials = await metadataService.GetMaterialsAsync().ConfigureAwait(false);
Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
Dictionary<WeaponId, Model.Metadata.Weapon.Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
List<BindingCultivateEntry> results = new();
List<CultivateEntry> entries = await appDbContext.CultivateEntries
.Where(e => e.ProjectId == projectId)
.Where(e => e.ProjectId == cultivateProject.InnerId)
.ToListAsync()
.ConfigureAwait(false);
@@ -153,13 +158,8 @@ internal class CultivationService : ICultivationService
Guid entryId = entry.InnerId;
List<BindingCultivateItem> resultItems = new();
List<CultivateItem> items = await appDbContext.CultivateItems
.Where(i => i.EntryId == entryId)
.OrderBy(i => i.ItemId)
.ToListAsync()
.ConfigureAwait(false);
foreach (CultivateItem item in items)
foreach (CultivateItem item in await GetEntryItemsAsync(appDbContext, entryId).ConfigureAwait(false))
{
resultItems.Add(new(materials.Single(m => m.Id == item.ItemId), item));
}
@@ -174,71 +174,64 @@ internal class CultivationService : ICultivationService
results.Add(new(entry, itemBase, resultItems));
}
return new(results.OrderByDescending(e => e.Items.Any(i => i.IsToday)));
return results
.OrderByDescending(e => e.Items.Any(i => i.IsToday))
.ToObservableCollection();
}
}
/// <inheritdoc/>
public async Task<List<Model.Binding.Cultivation.StatisticsCultivateItem>> GetStatisticsCultivateItemsAsync(CultivateProject cultivateProject, List<Model.Metadata.Material> materials)
public async Task<ObservableCollection<BindingStatisticsItem>> GetStatisticsCultivateItemCollectionAsync(CultivateProject cultivateProject, CancellationToken token)
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
List<BindingStatisticsItem> resultItems = new();
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
List<Model.Metadata.Material> materials = await scope.ServiceProvider
.GetRequiredService<IMetadataService>()
.GetMaterialsAsync(default)
.ConfigureAwait(false);
Guid projectId = cultivateProject.InnerId;
List<Model.Binding.Cultivation.StatisticsCultivateItem> resultItems = new();
token.ThrowIfCancellationRequested();
List<CultivateEntry> entries = await appDbContext.CultivateEntries
.AsNoTracking()
.Where(e => e.ProjectId == projectId)
.ToListAsync()
.ConfigureAwait(false);
foreach (CultivateEntry entry in entries)
foreach (CultivateEntry entry in await GetProjectEntriesAsync(appDbContext, projectId).ConfigureAwait(false))
{
Guid entryId = entry.InnerId;
List<CultivateItem> items = await appDbContext.CultivateItems
.AsNoTracking()
.Where(i => i.EntryId == entryId)
.OrderBy(i => i.ItemId)
.ToListAsync()
.ConfigureAwait(false);
foreach (CultivateItem item in items)
foreach (CultivateItem item in await GetEntryItemsAsync(appDbContext, entry.InnerId).ConfigureAwait(false))
{
if (item.IsFinished)
{
continue;
}
if (resultItems.SingleOrDefault(i => i.Inner.Id == item.ItemId) is Model.Binding.Cultivation.StatisticsCultivateItem inPlaceItem)
if (resultItems.SingleOrDefault(i => i.Inner.Id == item.ItemId) is BindingStatisticsItem existedItem)
{
inPlaceItem.Count += item.Count;
existedItem.Count += item.Count;
}
else
{
resultItems.Add(new(materials.Single(m => m.Id == item.ItemId), item));
resultItems.Add(new(materials!.Single(m => m.Id == item.ItemId), item));
}
}
}
List<InventoryItem> inventoryItems = await appDbContext.InventoryItems
.AsNoTracking()
.Where(e => e.ProjectId == projectId)
.ToListAsync()
.ConfigureAwait(false);
token.ThrowIfCancellationRequested();
foreach (InventoryItem inventoryItem in inventoryItems)
foreach (InventoryItem inventoryItem in await GetProjectInventoryAsync(appDbContext, projectId).ConfigureAwait(false))
{
if (resultItems.SingleOrDefault(i => i.Inner.Id == inventoryItem.ItemId) is Model.Binding.Cultivation.StatisticsCultivateItem inPlaceItem)
if (resultItems.SingleOrDefault(i => i.Inner.Id == inventoryItem.ItemId) is BindingStatisticsItem existedItem)
{
inPlaceItem.TotalCount += inventoryItem.Count;
existedItem.TotalCount += inventoryItem.Count;
}
}
return resultItems.OrderByDescending(i => i.Count).ToList();
token.ThrowIfCancellationRequested();
await ThreadHelper.SwitchToMainThreadAsync();
return resultItems.OrderByDescending(i => i.Count).ToObservableCollection();
}
}
@@ -246,9 +239,11 @@ internal class CultivationService : ICultivationService
public async Task RemoveCultivateEntryAsync(Guid entryId)
{
await ThreadHelper.SwitchToBackgroundAsync();
IEnumerable<CultivateItem> removed;
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
removed = await GetEntryItemsAsync(appDbContext, entryId).ConfigureAwait(false);
await appDbContext.CultivateEntries.Where(i => i.InnerId == entryId).ExecuteDeleteAsync().ConfigureAwait(false);
}
}
@@ -309,4 +304,29 @@ internal class CultivationService : ICultivationService
return true;
}
private static Task<List<InventoryItem>> GetProjectInventoryAsync(AppDbContext appDbContext, Guid projectId)
{
return appDbContext.InventoryItems
.AsNoTracking()
.Where(e => e.ProjectId == projectId)
.ToListAsync();
}
private static Task<List<CultivateEntry>> GetProjectEntriesAsync(AppDbContext appDbContext, Guid projectId)
{
return appDbContext.CultivateEntries
.AsNoTracking()
.Where(e => e.ProjectId == projectId)
.ToListAsync();
}
private static Task<List<CultivateItem>> GetEntryItemsAsync(AppDbContext appDbContext, Guid entryId)
{
return appDbContext.CultivateItems
.AsNoTracking()
.Where(i => i.EntryId == entryId)
.OrderBy(i => i.ItemId)
.ToListAsync();
}
}

View File

@@ -24,11 +24,8 @@ internal interface ICultivationService
/// 获取绑定用的养成列表
/// </summary>
/// <param name="cultivateProject">养成计划</param>
/// <param name="materials">材料</param>
/// <param name="idAvatarMap">Id角色映射</param>
/// <param name="idWeaponMap">Id武器映射</param>
/// <returns>绑定用的养成列表</returns>
Task<ObservableCollection<Model.Binding.Cultivation.CultivateEntry>> GetCultivateEntriesAsync(CultivateProject cultivateProject, List<Material> materials, Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> idAvatarMap, Dictionary<WeaponId, Model.Metadata.Weapon.Weapon> idWeaponMap);
Task<ObservableCollection<Model.Binding.Cultivation.CultivateEntry>> GetCultivateEntriesAsync(CultivateProject cultivateProject);
/// <summary>
/// 获取物品列表
@@ -48,9 +45,9 @@ internal interface ICultivationService
/// 异步获取统计物品列表
/// </summary>
/// <param name="cultivateProject">养成计划</param>
/// <param name="materials">元数据</param>
/// <param name="token">取消令牌</param>
/// <returns>统计物品列表</returns>
Task<List<StatisticsCultivateItem>> GetStatisticsCultivateItemsAsync(CultivateProject cultivateProject, List<Material> materials);
Task<ObservableCollection<StatisticsCultivateItem>> GetStatisticsCultivateItemCollectionAsync(CultivateProject cultivateProject, CancellationToken token);
/// <summary>
/// 删除养成清单

View File

@@ -10,6 +10,7 @@ using Snap.Hutao.Message;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
@@ -47,6 +48,7 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
{
ThreadHelper.InvokeOnMainThread(() =>
{
// Database items have been deleted by cascade deleting.
entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
});
}
@@ -110,7 +112,7 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService<GameRecordClient>();
if (appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, SettingEntryHelper.FalseString).GetBoolean()
&& Ioc.Default.GetRequiredService<IGameService>().IsGameRunning())
&& scope.ServiceProvider.GetRequiredService<IGameService>().IsGameRunning())
{
// Prevent notify when we are in silent mode.
notify = false;
@@ -122,9 +124,9 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
.GetDailyNoteAsync(new(entry.User, entry.Uid))
.ConfigureAwait(false);
if (dailyNoteResponse.IsOk())
if (dailyNoteResponse.ReturnCode == 0)
{
WebDailyNote dailyNote = dailyNoteResponse.Data;
WebDailyNote dailyNote = dailyNoteResponse.Data!;
// database
entry.DailyNote = dailyNote;
@@ -138,6 +140,16 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
await new DailyNoteNotifier(scopeFactory, entry).NotifyAsync().ConfigureAwait(false);
}
}
// special retcode handling for dailynote
else if (dailyNoteResponse.ReturnCode == (int)Web.Response.KnownReturnCode.CODE1034)
{
scope.ServiceProvider.GetRequiredService<IInfoBarService>().Warning(dailyNoteResponse.ToString());
}
else
{
scope.ServiceProvider.GetRequiredService<IInfoBarService>().Error(dailyNoteResponse.ToString());
}
}
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
@@ -145,13 +157,14 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
}
/// <inheritdoc/>
public void RemoveDailyNote(DailyNoteEntry entry)
public async Task RemoveDailyNoteAsync(DailyNoteEntry entry)
{
entries!.Remove(entry);
using (IServiceScope scope = scopeFactory.CreateScope())
{
scope.ServiceProvider.GetRequiredService<AppDbContext>().DailyNotes.RemoveAndSave(entry);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.DailyNotes.ExecuteDeleteWhereAsync(d => d.InnerId == entry.InnerId).ConfigureAwait(false);
}
}
}

View File

@@ -36,5 +36,6 @@ public interface IDailyNoteService
/// 移除指定的实时便笺
/// </summary>
/// <param name="entry">指定的实时便笺</param>
void RemoveDailyNote(DailyNoteEntry entry);
/// <returns>任务</returns>
Task RemoveDailyNoteAsync(DailyNoteEntry entry);
}

View File

@@ -74,11 +74,8 @@ public static class GachaStatisticsExtensions
private static Color GetColorByName(string name)
{
byte[] codes = MD5.HashData(Encoding.UTF8.GetBytes(name));
Span<byte> first = new(codes, 0, 5);
Span<byte> second = new(codes, 5, 5);
Span<byte> third = new(codes, 10, 5);
Color color = Color.FromArgb(255, first.Average(), second.Average(), third.Average());
Span<byte> codes = MD5.HashData(Encoding.UTF8.GetBytes(name));
Color color = Color.FromArgb(255, codes.Slice(0, 5).Average(), codes.Slice(5, 5).Average(), codes.Slice(10, 5).Average());
return color;
}
}

View File

@@ -54,7 +54,8 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
bool isEmptyHistoryWishVisible = entry.GetBoolean();
IOrderedEnumerable<GachaItem> orderedItems = items.OrderBy(i => i.Id);
return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap, isEmptyHistoryWishVisible)).ConfigureAwait(false);
await ThreadHelper.SwitchToBackgroundAsync();
return CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap, isEmptyHistoryWishVisible);
}
private static GachaStatistics CreateCore(

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
@@ -116,7 +117,14 @@ internal class GachaLogService : IGachaLogService
public async Task<ObservableCollection<GachaArchive>> GetArchiveCollectionAsync()
{
await ThreadHelper.SwitchToMainThreadAsync();
return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
try
{
return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
}
catch (SqliteException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException($"无法获取祈愿记录: {ex.Message}", ex);
}
}
/// <inheritdoc/>
@@ -296,6 +304,7 @@ internal class GachaLogService : IGachaLogService
break;
}
token.ThrowIfCancellationRequested();
SaveGachaItems(itemsToAdd, isLazy, archive, configration.EndId);
await RandomDelayAsync(token).ConfigureAwait(false);
}
@@ -327,14 +336,21 @@ internal class GachaLogService : IGachaLogService
if (archive != null)
{
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId)
.Where(i => i.QueryType == configType)
.OrderByDescending(i => i.Id)
.FirstOrDefault();
try
{
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId)
.Where(i => i.QueryType == configType)
.OrderByDescending(i => i.Id)
.FirstOrDefault();
}
catch (SqliteException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("无法获取祈愿记录 End Id", ex);
}
}
return item?.Id ?? 0L;
@@ -369,11 +385,12 @@ internal class GachaLogService : IGachaLogService
private INameQuality GetNameQualityByItemId(int id)
{
return id.Place() switch
int place = id.Place();
return place switch
{
8 => idAvatarMap![id],
5 => idWeaponMap![id],
_ => throw Must.NeverHappen(),
_ => throw Must.NeverHappen($"Id places: {place}"),
};
}

View File

@@ -25,12 +25,6 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
this.gameService = gameService;
}
private enum UrlMatch
{
Chinese,
Oversea,
}
/// <inheritdoc/>
public string Name { get => nameof(GachaLogUrlWebCacheProvider); }
@@ -41,11 +35,12 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
/// <returns>缓存文件路径</returns>
public static string GetCacheFile(string path)
{
string folder = Path.GetDirectoryName(path) ?? string.Empty;
string cacheDataPathChinese = Path.Combine(folder, @"YuanShen_Data\webCaches\Cache\Cache_Data\data_2");
string cacheDataPathOversea = Path.Combine(folder, @"GenshinImpact_Data\webCaches\Cache\Cache_Data\data_2");
string exeName = Path.GetFileName(path);
string dataFolder = exeName == GameConstants.GenshinImpactFileName
? GameConstants.GenshinImpactData
: GameConstants.YuanShenData;
return File.Exists(cacheDataPathChinese) ? cacheDataPathChinese : cacheDataPathOversea;
return Path.Combine(Path.GetDirectoryName(path)!, dataFolder, @"webCaches\Cache\Cache_Data\data_2");
}
/// <inheritdoc/>
@@ -69,8 +64,8 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
using (MemoryStream memoryStream = new())
{
await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
string? result = Match(memoryStream, !tempFile.Path.Contains("GenshinImpact_Data"));
return new(!string.IsNullOrEmpty(result), result!);
string? result = Match(memoryStream, cacheFile.Contains(GameConstants.GenshinImpactData));
return new(!string.IsNullOrEmpty(result), result ?? "未找到可用的 Url");
}
}
}
@@ -98,4 +93,4 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
return null;
}
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 游戏常量
/// </summary>
internal static class GameConstants
{
/// <summary>
/// 设置文件
/// </summary>
public const string ConfigFileName = "config.ini";
/// <summary>
/// 国服文件名
/// </summary>
public const string YuanShenFileName = "YuanShen.exe";
/// <summary>
/// 外服文件名
/// </summary>
public const string GenshinImpactFileName = "GenshinImpact.exe";
/// <summary>
/// 国服数据文件夹
/// </summary>
public const string YuanShenData = "YuanShen_Data";
/// <summary>
/// 国际服数据文件夹
/// </summary>
public const string GenshinImpactData = "GenshinImpact_Data";
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 游戏文件操作异常
/// </summary>
internal class GameFileOperationException : Exception
{
/// <summary>
/// 构造一个新的用户数据损坏异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public GameFileOperationException(string message, Exception innerException)
: base($"游戏文件操作失败: {message}", innerException)
{
}
}

View File

@@ -12,11 +12,15 @@ using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.Unlocker;
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;
@@ -24,13 +28,14 @@ namespace Snap.Hutao.Service.Game;
/// 游戏服务
/// </summary>
[Injection(InjectAs.Singleton, typeof(IGameService))]
internal class GameService : IGameService, IDisposable
[SuppressMessage("", "CA1001")]
internal class GameService : IGameService
{
private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}";
private const string ConfigFile = "config.ini";
private readonly IServiceScopeFactory scopeFactory;
private readonly IMemoryCache memoryCache;
private readonly PackageConverter packageConverter;
private readonly SemaphoreSlim gameSemaphore = new(1);
private ObservableCollection<GameAccount>? gameAccounts;
@@ -40,10 +45,12 @@ internal class GameService : IGameService, IDisposable
/// </summary>
/// <param name="scopeFactory">范围工厂</param>
/// <param name="memoryCache">内存缓存</param>
public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache)
/// <param name="packageConverter">游戏文件包转换器</param>
public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache, PackageConverter packageConverter)
{
this.scopeFactory = scopeFactory;
this.memoryCache = memoryCache;
this.packageConverter = packageConverter;
}
/// <inheritdoc/>
@@ -141,7 +148,12 @@ internal class GameService : IGameService, IDisposable
public MultiChannel GetMultiChannel()
{
string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFile);
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
if (!File.Exists(configPath))
{
return new(null, null, configPath);
}
using (FileStream stream = File.OpenRead(configPath))
{
@@ -154,15 +166,30 @@ internal class GameService : IGameService, IDisposable
}
/// <inheritdoc/>
public void SetMultiChannel(LaunchScheme scheme)
public bool SetMultiChannel(LaunchScheme scheme)
{
string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile);
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFileName);
List<IniElement> elements;
using (FileStream readStream = File.OpenRead(configPath))
try
{
elements = IniSerializer.Deserialize(readStream).ToList();
using (FileStream readStream = File.OpenRead(configPath))
{
elements = IniSerializer.Deserialize(readStream).ToList();
}
}
catch (FileNotFoundException ex)
{
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", ex);
}
catch (DirectoryNotFoundException ex)
{
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", ex);
}
catch (UnauthorizedAccessException ex)
{
throw new GameFileOperationException($"无法读取或保存配置文件,请以管理员模式重试。", ex);
}
bool changed = false;
@@ -198,10 +225,59 @@ internal class GameService : IGameService, IDisposable
IniSerializer.Serialize(writeStream, elements);
}
}
return changed;
}
/// <inheritdoc/>
public async Task<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
string gamePath = GetGamePathSkipLocator();
string gameFolder = Path.GetDirectoryName(gamePath)!;
string gameFileName = Path.GetFileName(gamePath);
progress.Report(new("查询游戏资源信息"));
Response<GameResource> response = await Ioc.Default
.GetRequiredService<ResourceClient>()
.GetResourceAsync(launchScheme)
.ConfigureAwait(false);
if (response.IsOk())
{
GameResource resource = response.Data;
if (!LaunchSchemeMatchesExecutable(launchScheme, 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;
OverwriteGamePath(Path.Combine(gameFolder, exeName));
}
else
{
// We can't start the game
// when we failed to convert game
return false;
}
}
if (!launchScheme.IsOversea)
{
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
}
return true;
}
return false;
}
/// <inheritdoc/>
[SuppressMessage("", "IDE0046")]
public bool IsGameRunning()
{
if (gameSemaphore.CurrentCount == 0)
@@ -209,19 +285,19 @@ internal class GameService : IGameService, IDisposable
return true;
}
return Process.GetProcessesByName("YuanShen.exe").Any()
|| Process.GetProcessesByName("GenshinImpact.exe").Any();
return Process.GetProcessesByName(YuanShenFileName).Any()
|| Process.GetProcessesByName(GenshinImpactFileName).Any();
}
/// <inheritdoc/>
public async Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync()
{
await ThreadHelper.SwitchToMainThreadAsync();
if (gameAccounts == null)
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await ThreadHelper.SwitchToMainThreadAsync();
gameAccounts = appDbContext.GameAccounts.AsNoTracking().ToObservableCollection();
}
}
@@ -304,7 +380,15 @@ internal class GameService : IGameService, IDisposable
string? registrySdk = GameAccountRegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
{
GameAccount? account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
GameAccount? account;
try
{
account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("已存在多个匹配账号,请先删除重复的账号", ex);
}
if (account == null)
{
@@ -383,9 +467,9 @@ internal class GameService : IGameService, IDisposable
}
}
/// <inheritdoc/>
public void Dispose()
private static bool LaunchSchemeMatchesExecutable(LaunchScheme launchScheme, string gameFileName)
{
gameSemaphore?.Dispose();
return (launchScheme.IsOversea && gameFileName == GenshinImpactFileName)
|| (!launchScheme.IsOversea && gameFileName == YuanShenFileName);
}
}

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Package;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game;
@@ -83,6 +84,14 @@ internal interface IGameService
/// <returns>任务</returns>
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
/// <summary>
/// 替换游戏资源
/// </summary>
/// <param name="launchScheme">目标启动方案</param>
/// <param name="progress">进度</param>
/// <returns>是否替换成功</returns>
Task<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
/// <summary>
/// 修改注册表中的账号信息
/// </summary>
@@ -94,5 +103,6 @@ internal interface IGameService
/// 设置多通道值
/// </summary>
/// <param name="scheme">方案</param>
void SetMultiChannel(LaunchScheme scheme);
/// <returns>是否更改了ini文件</returns>
bool SetMultiChannel(LaunchScheme scheme);
}

View File

@@ -18,14 +18,21 @@ public struct MultiChannel
/// </summary>
public string SubChannel;
/// <summary>
/// 配置文件路径 当不为 null 时则存在文件读写问题
/// </summary>
public string? ConfigFilePath;
/// <summary>
/// 构造一个新的多通道
/// </summary>
/// <param name="channel">通道</param>
/// <param name="subChannel">子通道</param>
public MultiChannel(string? channel, string? subChannel)
/// <param name="configFilePath">配置文件路径</param>
public MultiChannel(string? channel, string? subChannel, string? configFilePath = null)
{
Channel = channel ?? string.Empty;
SubChannel = subChannel ?? string.Empty;
ConfigFilePath = configFilePath;
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 转换方向
/// </summary>
internal enum ConvertDirection
{
/// <summary>
/// 国际服转国服
/// </summary>
OverseaToChinese,
/// <summary>
/// 国服转国际服
/// </summary>
ChineseToOversea,
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包操作
/// </summary>
[DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")]
internal class ItemOperationInfo
{
/// <summary>
/// 构造一个新的包操作
/// </summary>
/// <param name="type">操作类型</param>
/// <param name="target">目标</param>
/// <param name="cache">缓存</param>
public ItemOperationInfo(ItemOperationType type, VersionItem target, VersionItem cache)
{
Type = type;
Target = target.RemoteName;
MoveTo = cache.RemoteName;
Md5 = target.Md5;
TotalBytes = target.FileSize;
}
/// <summary>
/// 操作的类型
/// </summary>
public ItemOperationType Type { get; set; }
/// <summary>
/// 目标文件
/// </summary>
public string Target { get; set; }
/// <summary>
/// 移动至中时的名称
/// </summary>
public string MoveTo { get; set; }
/// <summary>
/// 文件的目标Md5
/// </summary>
public string Md5 { get; set; }
/// <summary>
/// 文件的目标大小 Byte
/// </summary>
public long TotalBytes { get; set; }
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包文件操作的类型
/// </summary>
internal enum ItemOperationType
{
/// <summary>
/// 添加
/// </summary>
Add,
/// <summary>
/// 删除
/// </summary>
Remove,
/// <summary>
/// 替换
/// </summary>
Replace,
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包转换异常
/// </summary>
public class PackageConvertException : Exception
{
/// <inheritdoc cref="Exception.Exception(string?, Exception?)"/>
public PackageConvertException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -0,0 +1,396 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 游戏文件包转换器
/// </summary>
[HttpClient(HttpClientConfigration.Default)]
internal class PackageConverter
{
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
/// <summary>
/// 构造一个新的游戏文件转换器
/// </summary>
/// <param name="resourceClient">资源客户端</param>
/// <param name="options">Json序列化选项</param>
/// <param name="httpClient">http客户端</param>
public PackageConverter(JsonSerializerOptions options, HttpClient httpClient)
{
this.options = options;
this.httpClient = httpClient;
}
/// <summary>
/// 异步检查替换游戏资源
/// 调用前需要确认本地文件与服务器上的不同
/// </summary>
/// <param name="targetScheme">目标启动方案</param>
/// <param name="gameResouce">游戏资源</param>
/// <param name="gameFolder">游戏目录</param>
/// <param name="progress">进度</param>
/// <returns>替换结果与资源</returns>
public async Task<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResouce, string gameFolder, IProgress<PackageReplaceStatus> progress)
{
await ThreadHelper.SwitchToBackgroundAsync();
string scatteredFilesUrl = gameResouce.Game.Latest.DecompressedPath;
Uri pkgVersionUri = new($"{scatteredFilesUrl}/pkg_version");
ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese;
progress.Report(new("获取 Package Version"));
Dictionary<string, VersionItem> remoteItems;
try
{
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false))
{
remoteItems = await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false);
}
}
catch (IOException ex)
{
throw new PackageConvertException("下载 Package Version 失败", ex);
}
Dictionary<string, VersionItem> localItems;
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, "pkg_version")))
{
localItems = await GetVersionItemsAsync(localSteam, direction, ConvertRemoteName).ConfigureAwait(false);
}
IEnumerable<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems);
return await ReplaceGameResourceAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
}
/// <summary>
/// 检查过时文件与Sdk
/// </summary>
/// <param name="resource">游戏资源</param>
/// <param name="gameFolder">游戏文件夹</param>
/// <returns>任务</returns>
public async Task EnsureDeprecatedFilesAndSdkAsync(GameResource resource, string gameFolder)
{
if (resource.DeprecatedFiles != null)
{
foreach (NameMd5 file in resource.DeprecatedFiles)
{
string filePath = Path.Combine(gameFolder, file.Name);
if (File.Exists(filePath))
{
File.Move(filePath, $"{filePath}.backup");
}
}
}
string sdkDllBackup = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll.backup");
string sdkDll = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll");
string sdkVersionBackup = Path.Combine(gameFolder, YuanShenData, "sdk_pkg_version.backup");
string sdkVersion = Path.Combine(gameFolder, YuanShenData, "sdk_pkg_version");
// Only bilibili's sdk is not null
if (resource.Sdk != null)
{
if (File.Exists(sdkDllBackup) && File.Exists(sdkVersionBackup))
{
FileOperation.Move(sdkDllBackup, sdkDll, false);
FileOperation.Move(sdkVersionBackup, sdkVersion, false);
}
else
{
using (Stream sdkWebStream = await httpClient.GetStreamAsync(resource.Sdk.Path).ConfigureAwait(false))
{
using (ZipArchive zip = new(sdkWebStream))
{
foreach (ZipArchiveEntry entry in zip.Entries)
{
if (entry.CompressedLength != 0)
{
string targetPath = Path.Combine(gameFolder, entry.FullName);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
entry.ExtractToFile(targetPath, true);
}
}
}
}
}
}
else
{
// backup
FileOperation.Move(sdkDll, sdkDllBackup, true);
FileOperation.Move(sdkVersion, sdkVersionBackup, true);
}
}
private static string ConvertRemoteName(string remoteName, ConvertDirection direction)
{
// 我们已经提前重命名了整个 Data 文件夹 所以需要将 RemoteName 中的 Data 同样替换
if (remoteName.StartsWith(YuanShenData) || remoteName.StartsWith(GenshinImpactData))
{
return direction switch
{
ConvertDirection.OverseaToChinese => $"{YuanShenData}{remoteName[GenshinImpactData.Length..]}",
ConvertDirection.ChineseToOversea => $"{GenshinImpactData}{remoteName[YuanShenData.Length..]}",
_ => remoteName,
};
}
return remoteName;
}
private static IEnumerable<ItemOperationInfo> GetItemOperationInfos(Dictionary<string, VersionItem> remote, Dictionary<string, VersionItem> local)
{
foreach ((string remoteName, VersionItem remoteItem) in remote)
{
if (local.TryGetValue(remoteName, out VersionItem? localItem))
{
if (remoteItem.Md5 != localItem.Md5)
{
// 本地发现了同名且不同MD5的项需要替换为服务器上的项
yield return new(ItemOperationType.Replace, remoteItem, localItem);
}
local.Remove(remoteName);
}
else
{
// 本地没有发现同名项
yield return new(ItemOperationType.Add, remoteItem, remoteItem);
}
}
IEnumerable<ItemOperationInfo> removes = local.Select(kvp => new ItemOperationInfo(ItemOperationType.Remove, kvp.Value, kvp.Value));
foreach (ItemOperationInfo item in removes)
{
yield return item;
}
}
private static void RenameDataFolder(string gameFolder, ConvertDirection direction)
{
string yuanShenData = Path.Combine(gameFolder, YuanShenData);
string genshinImpactData = Path.Combine(gameFolder, GenshinImpactData);
// We have check the exe path previously
// so we assume the data folder is present
if (direction == ConvertDirection.ChineseToOversea)
{
if (Directory.Exists(yuanShenData))
{
Directory.Move(yuanShenData, genshinImpactData);
}
}
else
{
if (Directory.Exists(genshinImpactData))
{
Directory.Move(genshinImpactData, yuanShenData);
}
}
}
private static void MoveToCache(string cacheFilePath, string targetFullPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)!);
File.Move(targetFullPath, cacheFilePath, true);
}
private static async Task CopyToWithProgressAsync(Stream source, Stream target, string name, long totalBytes, IProgress<PackageReplaceStatus> progress)
{
const int bufferSize = 81920;
long totalBytesRead = 0;
int bytesRead;
Memory<byte> buffer = new byte[bufferSize];
do
{
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
await target.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress.Report(new(name, totalBytesRead, totalBytes));
if (bytesRead <= 0)
{
break;
}
}
while (bytesRead > 0);
}
private async Task<bool> ReplaceGameResourceAsync(IEnumerable<ItemOperationInfo> operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress<PackageReplaceStatus> progress)
{
// 重命名 _Data 目录
try
{
RenameDataFolder(gameFolder, direction);
}
catch (IOException)
{
// Access to the path is denied.
// When user install the game in special folder like 'Program Files'
return false;
}
// Ensure cache folder
string cacheFolder = Path.Combine(gameFolder, "Screenshot", "HutaoCache");
// 执行下载与移动操作
foreach (ItemOperationInfo info in operations)
{
progress.Report(new($"{info.Target}"));
string targetFilePath = Path.Combine(gameFolder, info.Target);
string cacheFilePath = Path.Combine(cacheFolder, info.Target);
string moveToFilePath = Path.Combine(cacheFolder, info.MoveTo);
switch (info.Type)
{
case ItemOperationType.Add:
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info, progress).ConfigureAwait(false);
break;
case ItemOperationType.Replace:
{
MoveToCache(moveToFilePath, targetFilePath);
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info, progress).ConfigureAwait(false);
break;
}
case ItemOperationType.Remove:
MoveToCache(moveToFilePath, targetFilePath);
break;
default:
break;
}
}
// 重新下载所有 *pkg_version 文件
await ReplacePackageVersionsAsync(scatteredFilesUrl, gameFolder).ConfigureAwait(false);
return true;
}
private async Task ReplaceFromCacheOrWebAsync(string cacheFilePath, string targetFilePath, string scatteredFilesUrl, ItemOperationInfo info, IProgress<PackageReplaceStatus> progress)
{
if (File.Exists(cacheFilePath))
{
string remoteMd5 = await Digest.GetFileMd5Async(cacheFilePath).ConfigureAwait(false);
if (info.Md5 == remoteMd5.ToLowerInvariant() && new FileInfo(cacheFilePath).Length == info.TotalBytes)
{
// Valid, move it to target path
// There shouldn't be any file in the path/name
File.Move(cacheFilePath, targetFilePath, false);
return;
}
else
{
// Invalid file, delete it
File.Delete(cacheFilePath);
}
}
// Cache no item, download it anyway.
using (FileStream fileStream = File.Create(targetFilePath))
{
while (true)
{
using (HttpResponseMessage response = await httpClient.GetAsync($"{scatteredFilesUrl}/{info.Target}", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
long totalBytes = response.Content.Headers.ContentLength ?? 0;
using (Stream webStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
try
{
await CopyToWithProgressAsync(webStream, fileStream, info.Target, totalBytes, progress).ConfigureAwait(false);
fileStream.Seek(0, SeekOrigin.Begin);
string remoteMd5 = await Digest.GetStreamMd5Async(fileStream).ConfigureAwait(false);
if (info.Md5 == remoteMd5.ToLowerInvariant())
{
return;
}
}
catch
{
// System.IO.IOException: The response ended prematurely.
// System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream.
// We want to retry forever.
}
}
}
}
}
}
private async Task ReplacePackageVersionsAsync(string scatteredFilesUrl, string gameFolder)
{
foreach (string versionFilePath in Directory.EnumerateFiles(gameFolder, "*pkg_version"))
{
string versionFileName = Path.GetFileName(versionFilePath);
if (versionFileName == "sdk_pkg_version")
{
// Skiping the sdk_pkg_version file,
// it can't be claimed from remote.
continue;
}
using (FileStream versionFileStream = File.Create(versionFilePath))
{
using (Stream webStream = await httpClient.GetStreamAsync($"{scatteredFilesUrl}/{versionFileName}").ConfigureAwait(false))
{
await webStream.CopyToAsync(versionFileStream).ConfigureAwait(false);
}
}
}
}
private async Task<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream)
{
Dictionary<string, VersionItem> results = new();
using (StreamReader reader = new(stream))
{
while (await reader.ReadLineAsync().ConfigureAwait(false) is string raw)
{
if (!string.IsNullOrEmpty(raw))
{
VersionItem item = JsonSerializer.Deserialize<VersionItem>(raw, options)!;
results.Add(item.RemoteName, item);
}
}
}
return results;
}
private async Task<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream, ConvertDirection direction, Func<string, ConvertDirection, string> nameConverter)
{
Dictionary<string, VersionItem> results = new();
using (StreamReader reader = new(stream))
{
while (await reader.ReadLineAsync().ConfigureAwait(false) is string raw)
{
if (!string.IsNullOrEmpty(raw))
{
VersionItem item = JsonSerializer.Deserialize<VersionItem>(raw, options)!;
results.Add(nameConverter(item.RemoteName, direction), item);
}
}
}
return results;
}
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Common;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包更新状态
/// </summary>
public class PackageReplaceStatus
{
/// <summary>
/// 构造一个新的包更新状态
/// </summary>
/// <param name="description">描述</param>
public PackageReplaceStatus(string description)
{
Description = description;
}
/// <summary>
/// 构造一个新的包更新状态
/// </summary>
/// <param name="name">名称</param>
/// <param name="bytesRead">读取的字节数</param>
/// <param name="totalBytes">总字节数</param>
public PackageReplaceStatus(string name, long bytesRead, long totalBytes)
{
Percent = (double)bytesRead / totalBytes;
Description = $"{name}\n{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}";
}
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 是否有进度
/// </summary>
public bool IsIndeterminate { get => Percent < 0; }
/// <summary>
/// 进度
/// </summary>
public double Percent { get; set; } = -1;
/// <summary>
/// 克隆
/// </summary>
/// <returns>克隆的实例</returns>
public PackageReplaceStatus Clone()
{
// 进度需要在主线程上创建
return new(Description) { Percent = Percent };
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包版本项
/// </summary>
internal class VersionItem
{
/// <summary>
/// 服务器上的名称
/// </summary>
[JsonPropertyName("remoteName")]
public string RemoteName { get; set; } = default!;
/// <summary>
/// MD5校验值
/// </summary>
[JsonPropertyName("md5")]
public string Md5 { get; set; } = default!;
/// <summary>
/// 文件尺寸
/// </summary>
[JsonPropertyName("fileSize")]
public long FileSize { get; set; }
}

View File

@@ -1,11 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32.SafeHandles;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Win32;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
using Windows.Win32.System.Diagnostics.ToolHelp;
using static Windows.Win32.PInvoke;
@@ -75,25 +75,32 @@ internal class GameFpsUnlocker : IGameFpsUnlocker
private static unsafe MODULEENTRY32 FindModule(int processId, string moduleName)
{
using (SafeFileHandle snapshot = CreateToolhelp32Snapshot_SafeHandle(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)processId))
HANDLE snapshot = CreateToolhelp32Snapshot(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)processId);
try
{
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
MODULEENTRY32 entry = StructMarshal.MODULEENTRY32();
bool found = false;
// First module must be exe. Ignoring it.
for (Module32First(snapshot, ref entry); Module32Next(snapshot, ref entry);)
bool loop = Module32First(snapshot, &entry);
while (loop)
{
if (entry.th32ProcessID == processId && entry.szModule.AsString() == moduleName)
{
found = true;
break;
}
loop = Module32Next(snapshot, &entry);
}
return found ? entry : default;
}
finally
{
CloseHandle(snapshot);
}
}
private async Task<MODULEENTRY32> FindModuleAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit)

View File

@@ -0,0 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Win32;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
using Windows.Win32.System.Diagnostics.ToolHelp;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary>
/// 游戏帧率解锁器异常
/// </summary>
internal class GameFpsUnlockerException : Exception
{
/// <summary>
/// 构造一个新的用户数据损坏异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public GameFpsUnlockerException(Exception innerException)
: base($"解锁帧率失败: {innerException.Message}", innerException)
{
}
}

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Service.Metadata;
/// <summary>
/// 指示该类为元数据初始化器
/// </summary>
public interface IMetadataInitializer
public interface IMetadataServiceInitialization
{
/// <summary>
/// 异步初始化元数据

View File

@@ -2,9 +2,9 @@
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
@@ -20,7 +20,7 @@ namespace Snap.Hutao.Service.Metadata;
/// </summary>
[Injection(InjectAs.Singleton, typeof(IMetadataService))]
[HttpClient(HttpClientConfigration.Default)]
internal partial class MetadataService : IMetadataService, IMetadataInitializer, ISupportAsyncInitialization
internal partial class MetadataService : IMetadataService, IMetadataServiceInitialization
{
private const string MetaFileName = "Meta.json";
@@ -63,14 +63,11 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
Directory.CreateDirectory(metadataFolderPath);
}
/// <inheritdoc/>
public bool IsInitialized { get => isInitialized; private set => isInitialized = value; }
/// <inheritdoc/>
public async ValueTask<bool> InitializeAsync()
{
await initializeCompletionSource.Task.ConfigureAwait(false);
return IsInitialized;
return isInitialized;
}
/// <inheritdoc/>
@@ -79,7 +76,7 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
ValueStopwatch stopwatch = ValueStopwatch.StartNew();
logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion begin");
IsInitialized = await TryUpdateMetadataAsync(token).ConfigureAwait(false);
isInitialized = await TryUpdateMetadataAsync(token).ConfigureAwait(false);
initializeCompletionSource.SetResult();
logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion completed in {time}ms", stopwatch.GetElapsedTime().TotalMilliseconds);
@@ -135,9 +132,10 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
string fileFullName = $"{fileName}.json";
bool skip = false;
if (File.Exists(Path.Combine(metadataFolderPath, fileFullName)))
string fileFullPath = Path.Combine(metadataFolderPath, fileFullName);
if (File.Exists(fileFullPath))
{
skip = md5 == await GetFileMd5Async(fileFullName, token).ConfigureAwait(false);
skip = md5 == await Digest.GetFileMd5Async(fileFullPath, token).ConfigureAwait(false);
}
if (!skip)
@@ -149,18 +147,6 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
});
}
private async Task<string> GetFileMd5Async(string fileFullName, CancellationToken token)
{
using (FileStream stream = File.OpenRead(Path.Combine(metadataFolderPath, fileFullName)))
{
byte[] bytes = await MD5.Create()
.ComputeHashAsync(stream, token)
.ConfigureAwait(false);
return Convert.ToHexString(bytes);
}
}
private async Task DownloadMetadataAsync(string fileFullName, CancellationToken token)
{
Stream sourceStream = await httpClient
@@ -186,7 +172,7 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
private async ValueTask<T> FromCacheOrFileAsync<T>(string fileName, CancellationToken token)
where T : class
{
Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败");
Verify.Operation(isInitialized, "元数据服务尚未初始化,或初始化失败");
string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}";
if (memoryCache.TryGetValue(cacheKey, out object? value))
@@ -204,7 +190,6 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
private async ValueTask<Dictionary<TKey, TValue>> FromCacheAsDictionaryAsync<TKey, TValue>(string fileName, Func<TValue, TKey> keySelector, CancellationToken token)
where TKey : notnull
{
Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败");
string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}.Map.{typeof(TKey).Name}";
if (memoryCache.TryGetValue(cacheKey, out object? value))
@@ -218,9 +203,8 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
}
private async ValueTask<Dictionary<TKey, TValue>> FromCacheAsDictionaryAsync<TKey, TValue, TData>(string fileName, Func<TData, TKey> keySelector, Func<TData, TValue> valueSelector, CancellationToken token)
where TKey : notnull
where TKey : notnull
{
Verify.Operation(IsInitialized, "元数据服务尚未初始化,或初始化失败");
string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}.Map.{typeof(TKey).Name}.{typeof(TValue).Name}";
if (memoryCache.TryGetValue(cacheKey, out object? value))

View File

@@ -181,7 +181,7 @@ internal class NavigationService : INavigationService
/// <inheritdoc/>
public void GoBack()
{
Program.DispatcherQueue!.TryEnqueue(() =>
ThreadHelper.InvokeOnMainThread(() =>
{
bool canGoBack = Frame?.CanGoBack ?? false;

View File

@@ -76,7 +76,14 @@ internal class UserService : IUserService
if (currentUser != null)
{
currentUser.IsSelected = true;
appDbContext.Users.UpdateAndSave(currentUser.Entity);
try
{
appDbContext.Users.UpdateAndSave(currentUser.Entity);
}
catch (InvalidOperationException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException($"用户 {currentUser.UserInfo?.Uid} 状态保存失败", ex);
}
}
messenger.Send(message);
@@ -97,8 +104,15 @@ internal class UserService : IUserService
await ThreadHelper.SwitchToBackgroundAsync();
using (IServiceScope scope = scopeFactory.CreateScope())
{
// Note: cascade deleted dailynotes
await scope.ServiceProvider.GetRequiredService<AppDbContext>().Users.RemoveAndSaveAsync(user.Entity).ConfigureAwait(false);
try
{
// Note: cascade deleted dailynotes
await scope.ServiceProvider.GetRequiredService<AppDbContext>().Users.RemoveAndSaveAsync(user.Entity).ConfigureAwait(false);
}
catch (DbUpdateConcurrencyException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("用户已被其他功能删除", ex);
}
}
messenger.Send(new UserRemovedMessage(user.Entity));
@@ -124,7 +138,14 @@ internal class UserService : IUserService
}
userCollection = users.ToObservableCollection();
Current = users.SingleOrDefault(user => user.IsSelected);
try
{
Current = users.SingleOrDefault(user => user.IsSelected);
}
catch (InvalidOperationException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("无法设置当前用户", ex);
}
}
return userCollection;
@@ -157,8 +178,16 @@ internal class UserService : IUserService
{
if (userCollection != null)
{
// TODO: optimize match speed.
return userCollection.SelectMany(u => u.UserGameRoles).SingleOrDefault(r => r.GameUid == uid);
try
{
// TODO: optimize match speed.
return userCollection.SelectMany(u => u.UserGameRoles).SingleOrDefault(r => r.GameUid == uid);
}
catch (InvalidOperationException)
{
// Sequence contains more than one matching element
// TODO: return a specialize UserGameRole to indicate error
}
}
return null;

View File

@@ -22,7 +22,7 @@
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<PackageCertificateThumbprint>F8C2255969BEA4A681CED102771BF807856AEC02</PackageCertificateThumbprint>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<AppxSymbolPackageEnabled>True</AppxSymbolPackageEnabled>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundle>Never</AppxBundle>
@@ -37,9 +37,12 @@
<ItemGroup>
<None Remove="Control\Panel\PanelSelector.xaml" />
<None Remove="Control\Theme\FontStyle.xaml" />
<None Remove="LaunchGameWindow.xaml" />
<None Remove="NativeMethods.json" />
<None Remove="NativeMethods.txt" />
<None Remove="Resource\Font\CascadiaMono.ttf" />
<None Remove="Resource\Font\MiSans-Regular.ttf" />
<None Remove="Resource\Icon\UI_AchievementIcon_3_3.png" />
<None Remove="Resource\Icon\UI_BagTabIcon_Avatar.png" />
<None Remove="Resource\Icon\UI_BagTabIcon_Weapon.png" />
@@ -84,6 +87,7 @@
<None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" />
<None Remove="View\Dialog\GachaLogUrlDialog.xaml" />
<None Remove="View\Dialog\GameAccountNameDialog.xaml" />
<None Remove="View\Dialog\LaunchGamePackageConvertDialog.xaml" />
<None Remove="View\Dialog\LoginMihoyoBBSDialog.xaml" />
<None Remove="View\Dialog\SignInWebViewDialog.xaml" />
<None Remove="View\Dialog\UserDialog.xaml" />
@@ -96,6 +100,7 @@
<None Remove="View\Page\DailyNotePage.xaml" />
<None Remove="View\Page\GachaLogPage.xaml" />
<None Remove="View\Page\HutaoDatabasePage.xaml" />
<None Remove="View\Page\HutaoDatabasePresentPage.xaml" />
<None Remove="View\Page\LaunchGamePage.xaml" />
<None Remove="View\Page\LoginMihoyoUserPage.xaml" />
<None Remove="View\Page\SettingPage.xaml" />
@@ -122,7 +127,8 @@
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
<Content Include="Resource\Font\Segoe Fluent Icons.ttf" />
<Content Include="Resource\Font\CascadiaMono.ttf" />
<Content Include="Resource\Font\MiSans-Regular.ttf" />
<Content Include="Resource\Icon\UI_AchievementIcon_3_3.png" />
<Content Include="Resource\Icon\UI_BagTabIcon_Avatar.png" />
<Content Include="Resource\Icon\UI_BagTabIcon_Weapon.png" />
@@ -150,12 +156,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0-preview3" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -167,18 +173,18 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.65" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.138-beta">
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.188-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25267-preview" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25281-preview" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221209.1" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.435">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="TaskScheduler" Version="2.10.1" />
<PackageReference Include="WinUICommunity.SettingsUI" Version="3.0.0" />
<PackageReference Include="WinUICommunity.SettingsUI" Version="3.0.2" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
@@ -195,6 +201,16 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\LaunchGamePackageConvertDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\HutaoDatabasePresentPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\WelcomeView.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -415,4 +431,9 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Control\Style\FontStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@@ -1,21 +1,20 @@
<Page
x:Class="Snap.Hutao.View.Page.AnnouncementContentPage"
<UserControl
x:Class="Snap.Hutao.View.Control.AnnouncementContentViewer"
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"
ActualThemeChanged="PageActualThemeChanged"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
Loaded="OnLoaded"
mc:Ignorable="d">
<Page.Transitions>
<UserControl.Transitions>
<TransitionCollection>
<NavigationThemeTransition>
<DrillInNavigationTransitionInfo/>
</NavigationThemeTransition>
<EntranceThemeTransition/>
</TransitionCollection>
</Page.Transitions>
</UserControl.Transitions>
<WebView2
x:Name="WebView"
Margin="0,0,0,0"
DefaultBackgroundColor="Transparent"
IsRightTapEnabled="False"/>
</Page>
</UserControl>

View File

@@ -0,0 +1,164 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Control;
using Snap.Hutao.Core;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using Windows.System;
namespace Snap.Hutao.View.Control;
/// <summary>
/// 公告内容页面
/// </summary>
public sealed partial class AnnouncementContentViewer : Microsoft.UI.Xaml.Controls.UserControl
{
// apply in dark mode, Dark theme
private const string LightColor1 = "color:rgba(255,255,255,1)";
private const string LightColor2 = "color:rgba(238,238,238,1)";
private const string LightColor3 = "color:rgba(204,204,204,1)";
private const string LightColor4 = "color:rgba(198,196,191,1)";
private const string LightColor5 = "color:rgba(170,170,170,1)";
private const string LightAccentColor1 = "background-color: rgb(0,40,70)";
private const string LightAccentColor2 = "background-color: rgb(1,40,70)";
// find in content, Light theme
private const string DarkColor1 = "color:rgba(0,0,0,1)";
private const string DarkColor2 = "color:rgba(17,17,17,1)";
private const string DarkColor3 = "color:rgba(51,51,51,1)";
private const string DarkColor4 = "color:rgba(57,59,64,1)";
private const string DarkColor5 = "color:rgba(85,85,85,1)";
private const string DarkAccentColor1 = "background-color: rgb(255, 215, 185);";
private const string DarkAccentColor2 = "background-color: rgb(254, 245, 231);";
// support click open browser.
private const string MihoyoSDKDefinition = """
window.miHoYoGameJSSDK = {
openInBrowser: function(url){ window.chrome.webview.postMessage(url); },
openInWebview: function(url){ location.href = url }
}
""";
private static readonly DependencyProperty AnnouncementProperty = Property<AnnouncementContentViewer>.Depend<Announcement>(nameof(Announcement));
/// <summary>
/// 构造一个新的公告窗体
/// </summary>
public AnnouncementContentViewer()
{
InitializeComponent();
}
/// <summary>
/// 目标公告
/// </summary>
public Announcement Announcement
{
get { return (Announcement)GetValue(AnnouncementProperty); }
set { SetValue(AnnouncementProperty, value); }
}
private static string? GenerateHtml(Announcement? announcement, ElementTheme theme)
{
if (announcement == null)
{
return null;
}
string content = announcement.Content;
if (string.IsNullOrWhiteSpace(content))
{
return null;
}
content = content
.Replace(@"style=""vertical-align:middle;""", string.Empty)
.Replace(@"style=""border:none;vertical-align:middle;""", string.Empty);
bool isDarkMode = ThemeHelper.IsDarkMode(theme);
if (isDarkMode)
{
content = content
.Replace(DarkColor5, LightColor5)
.Replace(DarkColor4, LightColor4)
.Replace(DarkColor3, LightColor3)
.Replace(DarkColor2, LightColor2)
.Replace(DarkColor1, LightColor1)
.Replace(DarkAccentColor1, LightAccentColor1)
.Replace(DarkAccentColor2, LightAccentColor2);
}
string document = $$"""
<!DOCTYPE html>
<html>
<head>
<style>
body::-webkit-scrollbar {
display: none;
}
img{
border: none;
vertical-align: middle;
width: 100%;
}
</style>
</head>
<body style="{{(isDarkMode ? LightColor1 : DarkColor1)}}; background-color: transparent;">
<h3>{{announcement.Title}}</h3>
<img src="{{announcement.Banner}}" />
<br>
{{content}}
</body>
</html>
""";
return document;
}
private async Task LoadAnnouncementAsync()
{
try
{
await WebView.EnsureCoreWebView2Async();
CoreWebView2Settings settings = WebView.CoreWebView2.Settings;
settings.AreBrowserAcceleratorKeysEnabled = false;
settings.AreDefaultContextMenusEnabled = false;
settings.AreDevToolsEnabled = false;
WebView.CoreWebView2.WebMessageReceived += OnWebMessageReceived;
await WebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(MihoyoSDKDefinition);
}
catch (Exception)
{
return;
}
WebView.NavigateToString(GenerateHtml(Announcement, ActualTheme));
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
LoadAnnouncementAsync().SafeForget();
}
private void OnWebMessageReceived(CoreWebView2 coreWebView2, CoreWebView2WebMessageReceivedEventArgs args)
{
string url = args.TryGetWebMessageAsString();
if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri? uri))
{
Launcher.LaunchUriAsync(uri).AsTask().SafeForget();
}
}
}

View File

@@ -2,6 +2,7 @@
x:Class="Snap.Hutao.View.Control.StatisticsCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:CommunityToolkit.WinUI.UI.Converters"
xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -62,13 +63,16 @@
</Grid>
</DataTemplate>
<converters:BoolToObjectConverter
x:Key="BoolToBrushConverter"
FalseValue="{ThemeResource SystemFillColorCriticalBackgroundBrush}"
TrueValue="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<DataTemplate x:Key="OrangeGridTemplate" x:DataType="shmbg:SummaryItem">
<Grid Width="40" Margin="0,4,4,0">
<Border
Background="{StaticResource CardBackgroundFillColorDefault}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}"
Background="{Binding IsUp, Converter={StaticResource BoolToBrushConverter}}"
Style="{StaticResource BorderCardStyle}"
ToolTipService.ToolTip="{Binding TimeFormatted}">
<StackPanel>
<shvc:ItemIcon
@@ -76,13 +80,11 @@
Height="40"
Icon="{Binding Icon}"
Quality="QUALITY_ORANGE"/>
<!--<shci:CachedImage
Source="{Binding Icon}"
Height="40" Width="40"/>-->
<TextBlock
HorizontalAlignment="Center"
HorizontalTextAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding LastPull}"
TextTrimming="None"
TextWrapping="NoWrap">
<TextBlock.Foreground>
<SolidColorBrush Color="{Binding Color}"/>
@@ -94,7 +96,7 @@
</DataTemplate>
</UserControl.Resources>
<Border Background="{StaticResource CardBackgroundFillColorDefaultBrush}" CornerRadius="{StaticResource CompatCornerRadius}">
<Border Style="{StaticResource BorderCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
@@ -108,19 +110,23 @@
HorizontalContentAlignment="Stretch"
Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
CornerRadius="4,4,0,0"
IsExpanded="True">
<Expander.Resources>
<Thickness x:Key="ExpanderHeaderBorderThickness">0,0,0,1</Thickness>
</Expander.Resources>
<Expander.Header>
<Grid Grid.Row="0">
<Grid>
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding Name}"/>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<TextBlock
Margin="0,4,12,2"
VerticalAlignment="Bottom"
FontFamily="Consolas"
FontSize="24"
Margin="0,0,6,0"
VerticalAlignment="Center"
FontSize="20"
Text="{Binding TotalCount}"
Visibility="{Binding ElementName=DetailExpander, Path=IsExpanded, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<shcp:PanelSelector
@@ -135,7 +141,7 @@
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="0,4,0,4"
FontFamily="Consolas"
FontFamily="{StaticResource CascadiaMonoAndMiSans}"
FontSize="48"
Text="{Binding TotalCount}"/>
<TextBlock
@@ -149,12 +155,7 @@
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
Background="{StaticResource CardBackgroundFillColorDefault}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}">
<Border Grid.Column="0" Style="{StaticResource BorderCardStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
@@ -165,14 +166,13 @@
Width="40"
Height="40"
Margin="4"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
Foreground="{StaticResource OrangeBrush}"
IsIndeterminate="False"
Maximum="{Binding GuarenteeOrangeThreshold}"
Value="{Binding LastOrangePull}"/>
<TextBlock
Grid.Column="0"
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{StaticResource OrangeBrush}"
@@ -180,7 +180,6 @@
Text="{Binding LastOrangePull}"/>
<TextBlock
Grid.Column="1"
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{StaticResource OrangeBrush}"
@@ -188,12 +187,7 @@
Text="五星"/>
</Grid>
</Border>
<Border
Grid.Column="1"
Background="{StaticResource CardBackgroundFillColorDefault}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}">
<Border Grid.Column="1" Style="{StaticResource BorderCardStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
@@ -204,14 +198,13 @@
Width="40"
Height="40"
Margin="4"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
Foreground="{StaticResource PurpleBrush}"
IsIndeterminate="False"
Maximum="{Binding GuarenteePurpleThreshold}"
Value="{Binding LastPurplePull}"/>
<TextBlock
Grid.Column="0"
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{StaticResource PurpleBrush}"
@@ -219,7 +212,6 @@
Text="{Binding LastPurplePull}"/>
<TextBlock
Grid.Column="1"
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{StaticResource PurpleBrush}"
@@ -232,20 +224,17 @@
<StackPanel Margin="0,8,0,0" Orientation="Horizontal">
<TextBlock
HorizontalAlignment="Left"
FontFamily="Consolas"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding FromFormatted}"/>
<TextBlock
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontFamily="Consolas"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="-"/>
<TextBlock
HorizontalAlignment="Left"
FontFamily="Consolas"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding ToFormatted}"/>
@@ -260,7 +249,7 @@
Text="五星"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas"
FontFamily="{StaticResource CascadiaMonoAndMiSans}"
Foreground="{StaticResource OrangeBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding TotalOrangeFormatted}"/>
@@ -272,7 +261,7 @@
Text="四星"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas"
FontFamily="{StaticResource CascadiaMonoAndMiSans}"
Foreground="{StaticResource PurpleBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding TotalPurpleFormatted}"/>
@@ -284,7 +273,7 @@
Text="三星"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas"
FontFamily="{StaticResource CascadiaMonoAndMiSans}"
Foreground="{StaticResource BlueBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding TotalBlueFormatted}"/>
@@ -294,7 +283,6 @@
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="五星平均抽数"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas,MicroSoft YaHei UI"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding AverageOrangePullFormatted}"/>
</Grid>
@@ -302,19 +290,16 @@
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="UP 平均抽数"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas,MicroSoft YaHei UI"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding AverageUpOrangePullFormatted}"/>
</Grid>
<Grid Margin="0,2,0,0">
<TextBlock
HorizontalAlignment="Left"
FontFamily="Consolas,MicroSoft YaHei UI"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding MaxOrangePullFormatted}"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas,MicroSoft YaHei UI"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding MinOrangePullFormatted}"/>
</Grid>

View File

@@ -1,21 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Converters;
namespace Snap.Hutao.View.Converter;
/// <summary>
/// This class converts a collection size into a boolean value.
/// </summary>
public class EmptyCollectionToBoolConverter : EmptyCollectionToObjectConverter
{
/// <summary>
/// Initializes a new instance of the <see cref="EmptyCollectionToVisibilityConverter"/> class.
/// </summary>
public EmptyCollectionToBoolConverter()
{
EmptyValue = false;
NotEmptyValue = true;
}
}

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