Compare commits

..

49 Commits

Author SHA1 Message Date
Masterain
6c83cd3da5 Update azure-pipelines.yml for Azure Pipelines 2023-02-07 21:54:23 -08:00
DismissedLight
e60a04a2bc impl #117 2023-02-08 12:28:31 +08:00
DismissedLight
aec483510f fix #460 2023-02-08 10:07:10 +08:00
DismissedLight
c245fe654e add gacha import validation 2023-02-07 16:57:53 +08:00
DismissedLight
898d95bb1d add more globalization strings 2023-02-07 15:36:50 +08:00
DismissedLight
1df22e5b75 fix L10n issues 2023-02-07 14:44:36 +08:00
DismissedLight
332e09fef0 fix #439 2023-02-07 13:41:53 +08:00
DismissedLight
2a77daf2ca Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-07 10:21:51 +08:00
DismissedLight
8a47ea8727 fix enka api 2023-02-07 10:21:36 +08:00
DismissedLight
b3937ac810 Merge pull request #453 from DGP-Studio/l10n_main
New Crowdin updates
2023-02-06 16:49:06 +08:00
Masterain
ed5c52dc63 New translations SH.resx (English) 2023-02-06 00:44:03 -08:00
Masterain
461d139602 New translations SH.resx (English) 2023-02-05 22:49:55 -08:00
Masterain
164ec2af33 New translations SH.resx (Chinese Traditional) 2023-02-05 22:49:54 -08:00
Masterain
e30523c621 New translations SH.resx (Russian) 2023-02-05 22:49:53 -08:00
Masterain
11d0405102 New translations SH.resx (Japanese) 2023-02-05 22:49:52 -08:00
DismissedLight
a1c0b4f830 fix zh-cn showcase 2 [skip ci] 2023-02-06 14:46:04 +08:00
Masterain
e476ed5960 New translations SH.resx (English) 2023-02-05 22:21:50 -08:00
Masterain
ffc999360d New translations SH.resx (Chinese Traditional) 2023-02-05 22:21:49 -08:00
Masterain
84058011c7 New translations SH.resx (Russian) 2023-02-05 22:21:48 -08:00
Masterain
c18e0c40c5 New translations SH.resx (Japanese) 2023-02-05 22:21:48 -08:00
DismissedLight
ad78515094 fix zh-cn task [skip ci] 2023-02-06 14:20:10 +08:00
Masterain
38367a090d New translations SH.resx (English) 2023-02-05 22:12:29 -08:00
Masterain
ce30f609fb New translations SH.resx (Chinese Traditional) 2023-02-05 22:12:28 -08:00
Masterain
f4b9cc7c48 New translations SH.resx (Russian) 2023-02-05 22:12:27 -08:00
Masterain
7c2212f44c New translations SH.resx (Japanese) 2023-02-05 22:12:26 -08:00
DismissedLight
95eddef457 fix zh-cn showcase 2023-02-06 14:11:43 +08:00
Masterain
02447bc966 New translations SH.resx (English) 2023-02-05 21:49:52 -08:00
Masterain
fb88e33d16 New translations SH.resx (Chinese Traditional) 2023-02-05 21:49:51 -08:00
Masterain
5fa36416ef New translations SH.resx (Russian) 2023-02-05 21:49:50 -08:00
Masterain
7076caaa5d New translations SH.resx (Japanese) 2023-02-05 21:49:49 -08:00
DismissedLight
b7b1155cfc adjust achievement UI 2023-02-06 13:45:41 +08:00
Masterain
6351f2b460 Update SH.resx
- fix typo in based language
2023-02-05 20:09:28 -08:00
DismissedLight
35ac2f33ba Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-05 21:06:45 +08:00
DismissedLight
f8ff1988bb update readme 2023-02-05 21:06:41 +08:00
Masterain
907d70ba71 Update azure-pipelines.yml for Azure Pipelines
[skip ci]
2023-02-05 05:04:47 -08:00
Masterain
a5bdc17712 Update azure-pipelines.yml
[skip ci]
2023-02-05 04:50:00 -08:00
Masterain
f078d92f33 Update Crowdin configuration file 2023-02-05 04:46:19 -08:00
DismissedLight
f2d4f0f1d3 locale start 2023-02-05 19:52:00 +08:00
DismissedLight
fcde9b21ae fix #442 2023-02-03 20:05:32 +08:00
DismissedLight
24f09861fd locale zh-cn phase 2 2023-02-02 20:48:48 +08:00
DismissedLight
47708adc83 update readme 2023-02-02 16:35:48 +08:00
DismissedLight
79a254235a Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-01 20:28:40 +08:00
DismissedLight
d9bcb3b16b locale zh-cn 2023-02-01 20:28:32 +08:00
Masterain
cf7dd548a2 Update network-issue.yml 2023-01-30 17:34:07 -08:00
DismissedLight
04deeb7086 Create FUNDING.yml 2023-01-30 19:26:25 +08:00
DismissedLight
9fb2da41cd store migration 2023-01-30 16:22:54 +08:00
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
211 changed files with 13200 additions and 2633 deletions

13
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: https://afdian.net/a/DismissedLight

View File

@@ -21,7 +21,7 @@ body:
**在填写下面的问题之前请先使用我们的网络诊断工具**
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
- 你可以点击下面的链接以下载网络诊断工具:
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-tool.exe)
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-hutao.exe)
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe)
validations:
required: true

View File

@@ -1,12 +1,15 @@
# [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)
[![](https://get.microsoft.com/images/zh-cn%20light.svg)](https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52)
# 特别感谢
## 贡献
* [向我们提交 PR](https://github.com/DGP-Studio/Snap.Hutao/pulls)
* [在 Crowdin 上进行本地化](https://crowdin.com/project/snap-hutao)
## 特别感谢
* [HolographicHat](https://github.com/HolographicHat)
* [UIGF organization](https://uigf.org)
@@ -30,4 +33,13 @@
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
* [WinUICommunity/SettingsUI](https://github.com/WinUICommunity/SettingsUI)
* [WinUICommunity/SettingsUI](https://github.com/WinUICommunity/SettingsUI)
### 支撑项目
* [Snap.Hutao.Server](https://github.com/DGP-Studio/Snap.Hutao.Server)
* [Snap.Metadata](https://github.com/DGP-Studio/Snap.Metadata)
* [Snap.Data.Mapper](https://github.com/DGP-Studio/Snap.Data.Mapper)
## 近期活跃数据
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)

View File

@@ -17,6 +17,7 @@ trigger:
- azure-pipelines.yml
- .github/ISSUE_TEMPLATE/*.yml
- .github/workflows/*.yml
- src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
pr:
branches:
include:
@@ -27,6 +28,7 @@ pr:
- azure-pipelines.yml
- .github/ISSUE_TEMPLATE/*.yml
- .github/workflows/*.yml
- src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
pool:
@@ -125,18 +127,21 @@ steps:
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
- task: MsixSigning@1
name: signMsix
displayName: Sign MSIX package
inputs:
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
certificate: 'DGP_Studio_CI.pfx'
passwordVariable: 'pw'
condition: succeeded()
- task: PublishPipelineArtifact@1
displayName: 'Upload Output'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/'
artifact: 'Output'
publishLocation: 'pipeline'
#- task: PublishPipelineArtifact@1
# displayName: 'Upload Output'
# inputs:
# targetPath: '$(Build.ArtifactStagingDirectory)/'
# artifact: 'Output'
# publishLocation: 'pipeline'
- task: DownloadSecureFile@1
name: cerFile
@@ -145,7 +150,6 @@ steps:
secureFile: 'Snap.Hutao.CI.cer'
- task: GitHubRelease@1
condition: or(eq(variables['Build.Reason'], 'Manual'), eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'))
inputs:
gitHubConnection: 'github.com_Masterain'
repositoryName: 'DGP-Studio/Snap.Hutao'
@@ -178,4 +182,4 @@ steps:
displayName: Upload CI via Rclone
inputs:
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/Alpha/'
configPath: '$(RcloneConfigFile.secureFilePath)'
configPath: '$(RcloneConfigFile.secureFilePath)'

3
crowdin.yml Normal file
View File

@@ -0,0 +1,3 @@
files:
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.%locale%.resx

View File

@@ -25,4 +25,4 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</ItemGroup>
</Project>
</Project>

View File

@@ -50,8 +50,8 @@ Global
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.ActiveCfg = Release|x86
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Build.0 = Release|x86
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Deploy.0 = Release|x86
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|x64
@@ -66,8 +66,8 @@ Global
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.ActiveCfg = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.Build.0 = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.ActiveCfg = Debug|x64
@@ -78,8 +78,8 @@ Global
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.ActiveCfg = Release|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.Build.0 = Release|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection

View File

@@ -9,7 +9,7 @@
},
"pathSegment": {
"add": {
".*": [ ".cs" ]
".*": [ ".cs", ".resx" ]
}
},
"fileSuffixToExtension": {
@@ -19,11 +19,12 @@
},
"fileToFile": {
"add": {
".filenesting.json": [ "App.xaml.cs" ],
"app.manifest": [ "App.xaml.cs" ],
"Package.appxmanifest": [ "App.xaml.cs" ],
"GlobalUsing.cs": [ "Program.cs" ],
".filenesting.json": [ "Program.cs" ],
".editorconfig": [ "Program.cs" ]
"Package.appxmanifest": [ "App.xaml" ],
"Package.StoreAssociation.xml": [ "App.xaml" ],
".editorconfig": [ "Program.cs" ],
"GlobalUsing.cs": [ "Program.cs" ]
}
}
}

View File

@@ -111,6 +111,328 @@
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{StaticResource CompatCornerRadius}"/>
</Style>
<Style x:Key="WebView2ContentDialogStyle" TargetType="ContentDialog">
<Setter Property="Foreground" Value="{ThemeResource ContentDialogForeground}"/>
<Setter Property="Background" Value="{ThemeResource ContentDialogBackground}"/>
<Setter Property="BorderThickness" Value="{ThemeResource ContentDialogBorderWidth}"/>
<Setter Property="BorderBrush" Value="{ThemeResource ContentDialogBorderBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentDialog">
<Border x:Name="Container">
<Grid x:Name="LayoutRoot" Visibility="Collapsed">
<Rectangle x:Name="SmokeLayerBackground" Fill="{ThemeResource ContentDialogSmokeFill}"/>
<Border
x:Name="BackgroundElement"
MinWidth="{ThemeResource ContentDialogMinWidth}"
MinHeight="{ThemeResource ContentDialogMinHeight}"
MaxWidth="{ThemeResource ContentDialogMaxWidth}"
MaxHeight="{ThemeResource ContentDialogMaxHeight}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
BackgroundSizing="InnerBorderEdge"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
FlowDirection="{TemplateBinding FlowDirection}"
RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform>
<ScaleTransform x:Name="ScaleTransform"/>
</Border.RenderTransform>
<Grid x:Name="DialogSpace" CornerRadius="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer
x:Name="ContentScrollViewer"
HorizontalScrollBarVisibility="Disabled"
IsTabStop="False"
VerticalScrollBarVisibility="Disabled"
ZoomMode="Disabled">
<Grid
Padding="0"
BorderBrush="{ThemeResource ContentDialogSeparatorBorderBrush}"
BorderThickness="{ThemeResource ContentDialogSeparatorThickness}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ContentControl
x:Name="Title"
Margin="{ThemeResource ContentDialogTitleMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="{TemplateBinding Title}"
ContentTemplate="{TemplateBinding TitleTemplate}"
FontFamily="{StaticResource ContentControlThemeFontFamily}"
FontSize="20"
FontWeight="SemiBold"
Foreground="{TemplateBinding Foreground}"
IsTabStop="False">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
MaxLines="2"
TextWrapping="Wrap"/>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
<ContentPresenter
x:Name="Content"
Grid.Row="1"
Margin="0,0,0,8"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
FontFamily="{StaticResource ContentControlThemeFontFamily}"
FontSize="{StaticResource ControlContentThemeFontSize}"
Foreground="{TemplateBinding Foreground}"
TextWrapping="Wrap"/>
</Grid>
</ScrollViewer>
<Grid
x:Name="CommandSpace"
Grid.Row="1"
Padding="8,0,8,8"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
XYFocusKeyboardNavigation="Enabled">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="PrimaryColumn" Width="*"/>
<ColumnDefinition x:Name="FirstSpacer" Width="0"/>
<ColumnDefinition x:Name="SecondaryColumn" Width="0"/>
<ColumnDefinition x:Name="SecondSpacer" Width="{ThemeResource ContentDialogButtonSpacing}"/>
<ColumnDefinition x:Name="CloseColumn" Width="*"/>
</Grid.ColumnDefinitions>
<Button
x:Name="PrimaryButton"
HorizontalAlignment="Stretch"
Content="{TemplateBinding PrimaryButtonText}"
ElementSoundMode="FocusOnly"
IsEnabled="{TemplateBinding IsPrimaryButtonEnabled}"
IsTabStop="False"
Style="{TemplateBinding PrimaryButtonStyle}"/>
<Button
x:Name="SecondaryButton"
HorizontalAlignment="Stretch"
Content="{TemplateBinding SecondaryButtonText}"
ElementSoundMode="FocusOnly"
IsEnabled="{TemplateBinding IsSecondaryButtonEnabled}"
IsTabStop="False"
Style="{TemplateBinding SecondaryButtonStyle}"/>
<Button
x:Name="CloseButton"
Grid.Column="4"
HorizontalAlignment="Stretch"
Content="{TemplateBinding CloseButtonText}"
ElementSoundMode="FocusOnly"
IsTabStop="False"
Style="{TemplateBinding CloseButtonStyle}"/>
</Grid>
</Grid>
</Border>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DialogShowingStates">
<VisualStateGroup.Transitions>
<VisualTransition To="DialogHidden">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="False"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleX">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.0"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFastAnimationDuration}"
Value="1.05"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleY">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.0"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFastAnimationDuration}"
Value="1.05"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.0"/>
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0.0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition To="DialogShowing">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleX">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.05"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleY">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.05"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0.0"/>
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="DialogHidden"/>
<VisualState x:Name="DialogShowing">
<VisualState.Setters>
<Setter Target="PrimaryButton.IsTabStop" Value="True"/>
<Setter Target="SecondaryButton.IsTabStop" Value="True"/>
<Setter Target="CloseButton.IsTabStop" Value="True"/>
<Setter Target="LayoutRoot.Visibility" Value="Visible"/>
<Setter Target="BackgroundElement.TabFocusNavigation" Value="Cycle"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DialogShowingWithoutSmokeLayer">
<VisualState.Setters>
<Setter Target="PrimaryButton.IsTabStop" Value="True"/>
<Setter Target="SecondaryButton.IsTabStop" Value="True"/>
<Setter Target="CloseButton.IsTabStop" Value="True"/>
<Setter Target="LayoutRoot.Visibility" Value="Visible"/>
<Setter Target="LayoutRoot.Background" Value="{x:Null}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DialogSizingStates">
<VisualState x:Name="DefaultDialogSizing"/>
<VisualState x:Name="FullDialogSizing">
<VisualState.Setters>
<Setter Target="BackgroundElement.VerticalAlignment" Value="Stretch"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ButtonsVisibilityStates">
<VisualState x:Name="AllVisible">
<VisualState.Setters>
<Setter Target="FirstSpacer.Width" Value="{ThemeResource ContentDialogButtonSpacing}"/>
<Setter Target="SecondaryColumn.Width" Value="*"/>
<Setter Target="SecondaryButton.(Grid.Column)" Value="2"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NoneVisible">
<VisualState.Setters>
<Setter Target="CommandSpace.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PrimaryVisible">
<VisualState.Setters>
<Setter Target="PrimaryButton.(Grid.Column)" Value="4"/>
<Setter Target="SecondaryButton.Visibility" Value="Collapsed"/>
<Setter Target="CloseButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SecondaryVisible">
<VisualState.Setters>
<Setter Target="SecondaryButton.(Grid.Column)" Value="4"/>
<Setter Target="PrimaryButton.Visibility" Value="Collapsed"/>
<Setter Target="CloseButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="CloseVisible">
<VisualState.Setters>
<Setter Target="PrimaryButton.Visibility" Value="Collapsed"/>
<Setter Target="SecondaryButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PrimaryAndSecondaryVisible">
<VisualState.Setters>
<Setter Target="SecondaryButton.(Grid.Column)" Value="4"/>
<Setter Target="CloseButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PrimaryAndCloseVisible">
<VisualState.Setters>
<Setter Target="SecondaryButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SecondaryAndCloseVisible">
<VisualState.Setters>
<Setter Target="PrimaryButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DefaultButtonStates">
<VisualState x:Name="NoDefaultButton"/>
<VisualState x:Name="PrimaryAsDefaultButton">
<VisualState.Setters>
<Setter Target="PrimaryButton.Style" Value="{StaticResource AccentButtonStyle}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SecondaryAsDefaultButton">
<VisualState.Setters>
<Setter Target="SecondaryButton.Style" Value="{StaticResource AccentButtonStyle}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="CloseAsDefaultButton">
<VisualState.Setters>
<Setter Target="CloseButton.Style" Value="{StaticResource AccentButtonStyle}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DialogBorderStates">
<VisualState x:Name="NoBorder"/>
<VisualState x:Name="AccentColorBorder">
<VisualState.Setters>
<Setter Target="BackgroundElement.BorderBrush" Value="{ThemeResource SystemControlForegroundAccentBrush}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ItemsPanelTemplate -->
<ItemsPanelTemplate x:Key="ItemsStackPanelTemplate">
<ItemsStackPanel/>
@@ -120,4 +442,4 @@
</ItemsPanelTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>
</Application>

View File

@@ -49,8 +49,8 @@ public partial class App : Application
firstInstance.Activated += Activation.Activate;
ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate;
logger.LogInformation(EventIds.CommonLog, "Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.LocalCacheFolder.Path);
logger.LogInformation("Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
logger.LogInformation("Cache folder : {folder}", ApplicationData.Current.LocalCacheFolder.Path);
JumpListHelper.ConfigureAsync().SafeForget(logger);
}

View File

@@ -8,6 +8,8 @@ namespace Snap.Hutao.Control;
/// <summary>
/// 绑定探针
/// 用于处理特定情况下需要穿透数据上下文的工作
/// DependencyObject will dispose inner ReferenceTracker in any time
/// when object is not used anymore.
/// </summary>
public class BindingProxy : DependencyObject
{

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Control.Extension;
/// <summary>
/// 对话框扩展
/// </summary>
internal static class ContentDialogExtensions
internal static class ContentDialogExtension
{
/// <summary>
/// 阻止用户交互

View File

@@ -32,7 +32,7 @@ public class CachedImage : ImageEx
try
{
Verify.Operation(imageUri.Host != string.Empty, "无效的Uri");
Verify.Operation(imageUri.Host != string.Empty, SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
// check token state to determine whether the operation should be canceled.

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
@@ -11,6 +12,7 @@ using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Image;
@@ -24,7 +26,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
private static readonly DependencyProperty SourceProperty = Property<CompositionImage>.Depend(nameof(Source), default(Uri), OnSourceChanged);
private static readonly ConcurrentCancellationTokenSource<CompositionImage> LoadingTokenSource = new();
private readonly IImageCache imageCache;
private readonly IServiceProvider serviceProvider;
private SpriteVisual? spriteVisual;
private bool isShow = true;
@@ -34,8 +36,15 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
/// </summary>
public CompositionImage()
{
imageCache = Ioc.Default.GetRequiredService<IImageCache>();
serviceProvider = Ioc.Default.GetRequiredService<IServiceProvider>();
AllowFocusOnInteraction = false;
IsDoubleTapEnabled = false;
IsHitTestVisible = false;
IsHoldingEnabled = false;
IsRightTapEnabled = false;
IsTabStop = false;
SizeChanged += OnSizeChanged;
}
@@ -86,11 +95,11 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
if (exception is HttpRequestException httpRequestException)
{
infoBarService.Error(httpRequestException, $"GET {uri}");
infoBarService.Error(httpRequestException, string.Format(SH.ControlImageCompositionImageHttpRequest, uri));
}
else
{
infoBarService.Error(exception, $"应用 {nameof(CompositionImage)} 的源时发生异常");
infoBarService.Error(exception.GetBaseException(), SH.ControlImageCompositionImageSystemException);
}
}
@@ -124,26 +133,20 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
if (uri != null)
{
if (uri.Scheme == "ms-appx")
{
imageSurface = LoadedImageSurface.StartLoadFromUri(uri);
}
else
{
string storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
IImageCache imageCache = serviceProvider.GetRequiredService<IImageCache>();
string file = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
try
{
imageSurface = await LoadImageSurfaceAsync(storageFile, token).ConfigureAwait(true);
}
catch (COMException)
{
imageCache.Remove(uri.Enumerate());
}
catch (IOException)
{
imageCache.Remove(uri.Enumerate());
}
try
{
imageSurface = await LoadImageSurfaceAsync(file, token).ConfigureAwait(true);
}
catch (COMException)
{
imageCache.Remove(uri.Enumerate());
}
catch (IOException)
{
imageCache.Remove(uri.Enumerate());
}
if (imageSurface != null)

View File

@@ -6,6 +6,7 @@ using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Theme;
namespace Snap.Hutao.Control.Image;
@@ -49,12 +50,7 @@ public class MonoChrome : CompositionImage
private void SetBackgroundColor(CompositionColorBrush backgroundBrush)
{
ApplicationTheme theme = ActualTheme switch
{
ElementTheme.Light => ApplicationTheme.Light,
ElementTheme.Dark => ApplicationTheme.Dark,
_ => Ioc.Default.GetRequiredService<App>().RequestedTheme,
};
ApplicationTheme theme = ThemeHelper.ElementToApplication(ActualTheme);
backgroundBrush.Color = theme switch
{

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.Control.Markup;
/// <summary>
/// Xaml extension to return a <see cref="string"/> value from resource file associated with a resource key
/// </summary>
[MarkupExtensionReturnType(ReturnType = typeof(string))]
public sealed class ResourceStringExtension : MarkupExtension
{
/// <summary>
/// Gets or sets associated ID from resource strings.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets a string value from resource file associated with a resource key.
/// </summary>
/// <param name="name">Resource key name.</param>
/// <returns>A string value from resource file associated with a resource key.</returns>
public static string GetValue(string name)
{
// This function is needed to accomodate compiled function usage without second paramater,
// which doesn't work with optional values.
return SH.ResourceManager.GetString(name)!;
}
/// <inheritdoc/>
protected override object ProvideValue()
{
return GetValue(Name ?? string.Empty);
}
}

View File

@@ -9,8 +9,10 @@ namespace Snap.Hutao.Control.Panel;
/// <summary>
/// 纵横比控件
/// </summary>
internal class AspectRatio : Microsoft.UI.Xaml.Controls.ContentControl
internal class AspectRatio : Microsoft.UI.Xaml.Controls.Control
{
private const double Epsilon = 2.2204460492503131e-016;
private static readonly DependencyProperty TargetWidthProperty = Property<AspectRatio>.Depend(nameof(TargetWidth), 1D);
private static readonly DependencyProperty TargetHeightProperty = Property<AspectRatio>.Depend(nameof(TargetHeight), 1D);
@@ -38,6 +40,11 @@ internal class AspectRatio : Microsoft.UI.Xaml.Controls.ContentControl
double ratio = TargetWidth / TargetHeight;
double ratioAvailable = availableSize.Width / availableSize.Height;
if (Math.Abs(ratioAvailable - ratio) < Epsilon)
{
return availableSize;
}
// 更宽
if (ratioAvailable > ratio)
{

View File

@@ -21,12 +21,12 @@
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Tag="List"
Text="列表"/>
Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
<RadioMenuFlyoutItem
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Tag="Grid"
Text="网格"/>
Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>

View File

@@ -16,16 +16,34 @@ namespace Snap.Hutao.Control;
[SuppressMessage("", "CA1001")]
public class ScopedPage : Page
{
// Allow GC to Collect the IServiceScope
private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(null!);
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope serviceScope;
private readonly IServiceScope currentScope;
/// <summary>
/// 构造一个新的页面
/// </summary>
public ScopedPage()
{
serviceScope = Ioc.Default.CreateScope();
serviceScope.Track();
Unloaded += OnScopedPageUnloaded;
currentScope = Ioc.Default.CreateScope();
DisposePreviousScope();
// track current
PreviousScopeReference.SetTarget(currentScope);
}
/// <summary>
/// 释放上个范围
/// </summary>
public static void DisposePreviousScope()
{
if (PreviousScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
}
/// <summary>
@@ -36,7 +54,7 @@ public class ScopedPage : Page
public void InitializeWith<TViewModel>()
where TViewModel : class, IViewModel
{
IViewModel viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
@@ -59,7 +77,6 @@ public class ScopedPage : Page
/// <inheritdoc/>
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
using (viewCancellationTokenSource)
{
// Cancel all tasks executed by the view model
@@ -73,7 +90,7 @@ public class ScopedPage : Page
viewModel.IsViewDisposed = true;
// Dispose the scope
serviceScope.Dispose();
currentScope.Dispose();
}
}
}
@@ -81,11 +98,15 @@ public class ScopedPage : Page
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData extra)
{
NotifyRecipentAsync(extra).SafeForget();
}
}
private void OnScopedPageUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
DataContext = null;
Unloaded -= OnScopedPageUnloaded;
}
}

View File

@@ -9,7 +9,7 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Media;
using Snap.Hutao.Core;
using Snap.Hutao.Control.Theme;
using Windows.UI;
namespace Snap.Hutao.Control.Text;

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Core;
namespace Snap.Hutao.Control.Theme;
/// <summary>
/// 主题帮助工具类
@@ -42,6 +42,21 @@ public static class ThemeHelper
};
}
/// <summary>
/// 从 <see cref="ElementTheme"/> 转换到 <see cref="ApplicationTheme"/>
/// </summary>
/// <param name="applicationTheme">元素主题</param>
/// <returns>应用主题</returns>
public static ApplicationTheme ElementToApplication(ElementTheme applicationTheme)
{
return applicationTheme switch
{
ElementTheme.Light => ApplicationTheme.Light,
ElementTheme.Dark => ApplicationTheme.Dark,
_ => Ioc.Default.GetRequiredService<App>().RequestedTheme,
};
}
/// <summary>
/// 从 <see cref="ElementTheme"/> 转换到 <see cref="SystemBackdropTheme"/>
/// </summary>

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Snap.Hutao.Core.Annotation;
/// <summary>
/// 本地化键
/// </summary>
internal class LocalizationKeyAttribute : Attribute
{
/// <summary>
/// 指定本地化键
/// </summary>
/// <param name="key">键</param>
public LocalizationKeyAttribute(string key)
{
Key = key;
}
/// <summary>
/// 键
/// </summary>
public string Key { get; }
}

View File

@@ -3,6 +3,7 @@
using Microsoft.Win32;
using Snap.Hutao.Core.Json;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Collections.Immutable;
@@ -36,13 +37,13 @@ internal static class CoreEnvironment
/// 盐
/// </summary>
// https://github.com/UIGF-org/Hoyolab.Salt
public static readonly ImmutableDictionary<string, string> DynamicSecrets = new Dictionary<string, string>()
public static readonly ImmutableDictionary<SaltType, string> DynamicSecrets = new Dictionary<SaltType, string>()
{
[nameof(SaltType.K2)] = "dZAwGk4e9aC0MXXItkwnHamjA1x30IYw",
[nameof(SaltType.LK2)] = "IEIZiKYaput2OCKQprNuGsog1NZc1FkS",
[nameof(SaltType.X4)] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
[nameof(SaltType.X6)] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
[nameof(SaltType.PROD)] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
[SaltType.K2] = "dZAwGk4e9aC0MXXItkwnHamjA1x30IYw",
[SaltType.LK2] = "IEIZiKYaput2OCKQprNuGsog1NZc1FkS",
[SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
[SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
}.ToImmutableDictionary();
/// <summary>
@@ -70,6 +71,11 @@ internal static class CoreEnvironment
/// </summary>
public static readonly string FamilyName;
/// <summary>
/// 安装位置
/// </summary>
public static readonly string InstalledLocation;
/// <summary>
/// 数据文件夹
/// </summary>
@@ -98,9 +104,10 @@ internal static class CoreEnvironment
static CoreEnvironment()
{
DataFolder = GetDocumentsHutaoPath();
DataFolder = GetDatafolderPath();
Version = Package.Current.Id.Version.ToVersion();
FamilyName = Package.Current.Id.FamilyName;
InstalledLocation = Package.Current.InstalledLocation.Path;
CommonUA = $"Snap Hutao/{Version}";
// simply assign a random guid
@@ -115,18 +122,27 @@ internal static class CoreEnvironment
return Convert.ToMd5HexString($"{userName}{machineGuid}");
}
private static string GetDocumentsHutaoPath()
private static string GetDatafolderPath()
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
if (string.IsNullOrEmpty(preferredPath))
{
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";
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
Directory.CreateDirectory(path);
return path;
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
Directory.CreateDirectory(path);
return path;
}
else
{
return preferredPath;
}
}
}

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,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 可枚举扩展
/// </summary>
public static class EnumerableExtension
{
/// <summary>
/// 获取选中的值或默认值
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>选中的值或默认值</returns>
public static TSource? SelectedOrDefault<TSource>(this IEnumerable<TSource> source)
where TSource : ISelectable
{
return source.SingleOrDefault(i => i.IsSelected);
}
}

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

@@ -18,7 +18,7 @@ internal static class IocConfiguration
/// </summary>
/// <param name="services">集合</param>
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddJsonSerializerOptions(this IServiceCollection services)
public static IServiceCollection AddJsonOptions(this IServiceCollection services)
{
return services.AddSingleton(CoreEnvironment.JsonOptions);
}

View File

@@ -1,36 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 服务范围扩展
/// </summary>
public static class ServiceScopeExtension
{
// Allow GC to Collect the IServiceScope
private static readonly WeakReference<IServiceScope> ScopeReference = new(null!);
/// <summary>
/// 追踪服务范围
/// </summary>
/// <param name="scope">范围</param>
public static void Track(this IServiceScope scope)
{
DisposeLast();
ScopeReference.SetTarget(scope);
}
/// <summary>
/// 释放上个范围
/// </summary>
public static void DisposeLast()
{
if (ScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
}
}

View File

@@ -3,6 +3,8 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
using System.Collections;
using System.Text;
namespace Snap.Hutao.Core.ExceptionService;
@@ -33,12 +35,16 @@ internal class ExceptionRecorder
Ioc.Default.GetRequiredService<Web.Hutao.HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
#pragma warning restore VSTHRD002
#endif
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())
StringBuilder dataDetailBuilder = new();
foreach (DictionaryEntry entry in e.Exception.Data)
{
provider.Dispose();
string key = $"{entry.Key}";
string value = $"{entry.Value}";
dataDetailBuilder.Append(key).Append(':').Append(value).Append("\r\n");
}
logger.LogError(e.Exception, "未经处理的异常\r\n{detail}", dataDetailBuilder.ToString());
}
private void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e)

View File

@@ -0,0 +1,56 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Package;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 帮助更好的抛出异常
/// </summary>
[System.Diagnostics.StackTraceHidden]
internal static class ThrowHelper
{
/// <summary>
/// 操作取消
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <exception cref="OperationCanceledException">操作取消异常</exception>
/// <returns>nothing</returns>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static OperationCanceledException OperationCanceled(string message, Exception? inner)
{
throw new OperationCanceledException(message, inner);
}
/// <summary>
/// 包转换错误
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <returns>nothing</returns>
/// <exception cref="PackageConvertException">包转换错误异常</exception>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static PackageConvertException PackageConvert(string message, Exception inner)
{
throw new PackageConvertException(message, inner);
}
/// <summary>
/// 用户数据损坏
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <exception cref="UserdataCorruptedException">数据损坏</exception>
/// <returns>nothing</returns>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static UserdataCorruptedException UserdataCorrupted(string message, Exception inner)
{
throw new UserdataCorruptedException(message, inner);
}
}

View File

@@ -14,7 +14,7 @@ internal class UserdataCorruptedException : Exception
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public UserdataCorruptedException(string message, Exception innerException)
: base($"用户数据已损坏: {message}", innerException)
: base(string.Format(SH.CoreExceptionServiceUserdataCorruptedMessage, message), innerException)
{
}
}

View File

@@ -22,7 +22,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
/// </summary>
public const string JobNamePrefix = "SnapHutaoBitsJob";
private const uint BitsEngineNoProgressTimeout = 120;
private const uint BitsEngineNoProgressTimeout = 30;
private const int MaxResumeAttempts = 10;
private readonly string displayName;
@@ -30,7 +30,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
private readonly object lockObj = new();
private IBackgroundCopyJob? nativeJob;
private System.Exception? jobException;
private Exception? jobException;
private BG_JOB_PROGRESS progress;
private BG_JOB_STATE state;
private bool isJobComplete;
@@ -79,7 +79,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
UpdateJobState();
CompleteOrCancel();
}
catch (System.Exception ex)
catch (Exception ex)
{
log.LogInformation("Failed to job transfer: {message}", ex.Message);
}
@@ -101,7 +101,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
CompleteOrCancel();
log.LogInformation(jobException, "Job Exception:");
}
catch (System.Exception ex)
catch (Exception ex)
{
log?.LogInformation("Failed to handle job error: {message}", ex.Message);
}
@@ -141,7 +141,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
CompleteOrCancel();
}
}
catch (System.Exception ex)
catch (Exception ex)
{
log.LogInformation(ex, "message");
}
@@ -283,7 +283,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
{
action();
}
catch (System.Exception ex)
catch (Exception ex)
{
log.LogInformation("{name} failed. {exception}", displayName, ex);
if (throwOnFailure)

View File

@@ -39,7 +39,8 @@ internal class BitsManager
public async Task<ValueResult<bool, TempFile>> DownloadAsync(Uri uri, IProgress<ProgressUpdateStatus> progress, CancellationToken token = default)
{
TempFile tempFile = new(true);
bool result = await Task.Run(() => DownloadCore(uri, tempFile.Path, progress.Report, token), token).ConfigureAwait(false);
await ThreadHelper.SwitchToBackgroundAsync();
bool result = DownloadCore(uri, tempFile.Path, progress.Report, token);
return new(result, tempFile);
}

View File

@@ -1,11 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Core.IO.Bits;
/// <summary>
/// 进度更新状态
/// </summary>
[DebuggerDisplay("{BytesRead}/{TotalBytes}")]
public class ProgressUpdateStatus
{
/// <summary>

View File

@@ -20,7 +20,6 @@ internal static class Clipboard
public static async Task<T?> DeserializeTextAsync<T>(JsonSerializerOptions options)
where T : class
{
await ThreadHelper.SwitchToMainThreadAsync();
DataPackageView view = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent();
string json = await view.GetTextAsync();
return JsonSerializer.Deserialize<T>(json, options);

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

@@ -1,31 +0,0 @@
// 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 FileDigest
{
/// <summary>
/// 异步获取文件 Md5 摘要
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="token">取消令牌</param>
/// <returns>文件 Md5 摘要</returns>
public static async Task<string> GetMd5Async(string filePath, CancellationToken token)
{
using (FileStream stream = File.OpenRead(filePath))
{
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

@@ -32,13 +32,7 @@ internal static class PickerExtension
}
else
{
if (exception != null)
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning("无法打开文件选择器", $"请勿在管理员模式下使用此功能 {exception.Message}");
}
InfoBarWaringPickerException(exception);
return new(false, null!);
}
}
@@ -64,14 +58,46 @@ internal static class PickerExtension
}
else
{
if (exception != null)
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning("无法打开文件选择器", $"请勿在管理员模式下使用此功能 {exception.Message}");
}
InfoBarWaringPickerException(exception);
return new(false, null!);
}
}
/// <inheritdoc cref="FolderPicker.PickSingleFolderAsync"/>
public static async Task<ValueResult<bool, string>> TryPickSingleFolderAsync(this FolderPicker picker)
{
StorageFolder? folder;
Exception? exception = null;
try
{
folder = await picker.PickSingleFolderAsync().AsTask().ConfigureAwait(false);
}
catch (Exception ex)
{
exception = ex;
folder = null;
}
if (folder != null)
{
return new(true, folder.Path);
}
else
{
InfoBarWaringPickerException(exception);
return new(false, null!);
}
}
private static void InfoBarWaringPickerException(Exception? exception)
{
if (exception != null)
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning(
SH.CoreIOPickerExtensionPickerExceptionInfoBarTitle,
string.Format(SH.CoreIOPickerExtensionPickerExceptionInfoBarMessage, exception.Message));
}
}
}

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

@@ -23,8 +23,8 @@ public static class JumpListHelper
list.Items.Clear();
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, "启动游戏");
launchGameItem.GroupName = "快捷操作";
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, SH.CoreJumpListHelperLaunchGameItemDisplayName);
launchGameItem.GroupName = SH.CoreJumpListHelperLaunchGameItemGroupName;
launchGameItem.Logo = new("ms-appx:///Resource/Icon/UI_GuideIcon_PlayMethod.png");
list.Items.Add(launchGameItem);

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>

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// Extension methods for the <see cref="ILoggerFactory"/> class.
/// </summary>
public static class DatabaseLoggerFactoryExtensions
{
/// <summary>
/// Adds a debug logger named 'Debug' to the factory.
/// </summary>
/// <param name="builder">The extension method argument.</param>
/// <returns>日志构造器</returns>
public static ILoggingBuilder AddDatabase(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DatebaseLoggerProvider>());
return builder;
}
}

View File

@@ -1,80 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// A logger that writes messages in the database table
/// </summary>
internal sealed partial class DatebaseLogger : ILogger
{
private readonly string name;
private readonly LogEntryQueue logEntryQueue;
/// <summary>
/// Initializes a new instance of the <see cref="DatebaseLogger"/> class.
/// </summary>
/// <param name="name">The name of the logger.</param>
/// <param name="logEntryQueue">日志队列</param>
public DatebaseLogger(string name, LogEntryQueue logEntryQueue)
{
this.name = name;
this.logEntryQueue = logEntryQueue;
}
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state)
where TState : notnull
{
return new NullScope();
}
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
}
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, System.Exception? exception, Func<TState, System.Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
string message = formatter(state, exception);
if (string.IsNullOrEmpty(message))
{
return;
}
LogEntry entry = new()
{
Time = DateTimeOffset.Now,
Category = name,
LogLevel = logLevel,
EventId = eventId.Id,
Message = message,
Exception = exception?.ToString(),
};
logEntryQueue.Enqueue(entry);
}
/// <summary>
/// An empty scope without any logic
/// </summary>
private struct NullScope : IDisposable
{
public NullScope()
{
}
/// <inheritdoc />
public void Dispose()
{
}
}
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// The provider for the <see cref="DatebaseLogger"/>.
/// </summary>
[ProviderAlias("Database")]
public sealed class DatebaseLoggerProvider : ILoggerProvider
{
private readonly LogEntryQueue logEntryQueue = new();
/// <inheritdoc/>
public ILogger CreateLogger(string name)
{
return new DatebaseLogger(name, logEntryQueue);
}
/// <inheritdoc/>
public void Dispose()
{
logEntryQueue.Dispose();
}
}

View File

@@ -1,51 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// 数据库日志入口点
/// </summary>
[Table("logs")]
public class LogEntry
{
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 日志时间
/// </summary>
public DateTimeOffset Time { get; set; }
/// <summary>
/// 类别
/// </summary>
public string Category { get; set; } = default!;
/// <summary>
/// 日志等级
/// </summary>
public LogLevel LogLevel { get; set; }
/// <summary>
/// 事件Id
/// </summary>
public int EventId { get; set; }
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; } = default!;
/// <summary>
/// 可能的异常
/// </summary>
public string? Exception { get; set; }
}

View File

@@ -1,110 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.Concurrent;
using System.Diagnostics;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// 日志队列
/// </summary>
public sealed class LogEntryQueue : IDisposable
{
private readonly ConcurrentQueue<LogEntry> entryQueue = new();
private readonly CancellationTokenSource disposeTokenSource = new();
private readonly TaskCompletionSource writeDbCompletionSource = new();
private readonly LogDbContext logDbContext;
private bool disposed;
/// <summary>
/// 构造一个新的日志队列
/// </summary>
public LogEntryQueue()
{
logDbContext = InitializeDbContext();
Task.Run(() => WritePendingLogsAsync(disposeTokenSource.Token)).SafeForget();
}
/// <summary>
/// 将日志消息存入队列
/// </summary>
/// <param name="logEntry">日志</param>
public void Enqueue(LogEntry logEntry)
{
entryQueue.Enqueue(logEntry);
}
/// <inheritdoc/>
[SuppressMessage("", "VSTHRD002")]
public void Dispose()
{
if (disposed)
{
return;
}
// notify the write task to complete.
disposeTokenSource.Cancel();
// Wait the db operation complete.
writeDbCompletionSource.Task.GetAwaiter().GetResult();
logDbContext.Dispose();
disposed = true;
}
private static LogDbContext InitializeDbContext()
{
string logDbName = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Log.db");
LogDbContext logDbContext = LogDbContext.Create($"Data Source={logDbName}");
if (logDbContext.Database.GetPendingMigrations().Any())
{
Debug.WriteLine("[Debug] Performing LogDbContext Migrations");
logDbContext.Database.Migrate();
}
// only raw sql can pass
logDbContext.Logs.Where(log => log.Exception == null).ExecuteDelete();
return logDbContext;
}
private async Task WritePendingLogsAsync(CancellationToken token)
{
bool hasAdded = false;
while (true)
{
if (entryQueue.TryDequeue(out LogEntry? logEntry))
{
logDbContext.Logs.Add(logEntry);
hasAdded = true;
}
else
{
if (hasAdded)
{
logDbContext.SaveChanges();
hasAdded = false;
}
if (token.IsCancellationRequested)
{
writeDbCompletionSource.TrySetResult();
break;
}
try
{
await Task.Delay(5000, token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
}
}
}
}
}

View File

@@ -31,7 +31,7 @@ internal static class ScheduleTaskHelper
}
TaskDefinition task = TaskService.Instance.NewTask();
task.RegistrationInfo.Description = "胡桃实时便笺刷新任务 | 请勿编辑或删除。";
task.RegistrationInfo.Description = SH.CoreScheduleTaskHelperDailyNoteRefreshTaskDescription;
task.Triggers.Add(new TimeTrigger() { Repetition = new(TimeSpan.FromSeconds(interval), TimeSpan.Zero), });
task.Actions.Add("explorer", "hutao://DailyNote/Refresh");
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);

View File

@@ -77,6 +77,12 @@ internal static class LocalSetting
return Get<char>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static string Get(string key, string defaultValue)
{
return Get<string>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static DateTimeOffset Get(string key, DateTimeOffset defaultValue)
{
@@ -173,6 +179,12 @@ internal static class LocalSetting
Set<char>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, string value)
{
Set<string>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, DateTimeOffset value)
{
@@ -216,8 +228,7 @@ internal static class LocalSetting
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>获取的值</returns>
private static T Get<T>(string key, T defaultValue = default)
where T : struct
private static T Get<T>(string key, T defaultValue = default!)
{
if (Container.Values.TryGetValue(key, out object? value))
{
@@ -238,7 +249,6 @@ internal static class LocalSetting
/// <param name="key">键</param>
/// <param name="value">值</param>
private static void Set<T>(string key, T value)
where T : struct
{
Container.Values[key] = value;
}

View File

@@ -23,6 +23,11 @@ internal static class SettingKeys
/// </summary>
public const string LaunchTimes = "LaunchTimes";
/// <summary>
/// 数据文件夹
/// </summary>
public const string DataFolderPath = "DataFolderPath";
/// <summary>
/// 静态资源合约
/// 新增合约时 请注意

View File

@@ -1,12 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 信号量扩展
/// </summary>
public static class SemaphoreSlimExtensions
public static class SemaphoreSlimExtension
{
/// <summary>
/// 异步进入信号量
@@ -22,7 +24,7 @@ public static class SemaphoreSlimExtensions
}
catch (ObjectDisposedException ex)
{
throw new OperationCanceledException("信号量已经被释放,操作取消", ex);
ThrowHelper.OperationCanceled(SH.CoreThreadingSemaphoreSlimDisposed, ex);
}
return new SemaphoreSlimReleaser(semaphoreSlim);

View File

@@ -22,7 +22,7 @@ public static class TaskExtensions
{
await task.ConfigureAwait(false);
}
catch (System.Exception ex)
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
@@ -39,11 +39,11 @@ public static class TaskExtensions
{
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
// Do nothing
}
catch (System.Exception e)
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
}
@@ -55,17 +55,17 @@ public static class TaskExtensions
/// <param name="task">任务</param>
/// <param name="logger">日志器</param>
/// <param name="onException">发生异常时调用</param>
public static async void SafeForget(this Task task, ILogger? logger = null, Action<System.Exception>? onException = null)
public static async void SafeForget(this Task task, ILogger? logger = null, Action<Exception>? onException = null)
{
try
{
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
// Do nothing
}
catch (System.Exception e)
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
onException?.Invoke(e);
@@ -79,17 +79,17 @@ public static class TaskExtensions
/// <param name="logger">日志器</param>
/// <param name="onCanceled">任务取消时调用</param>
/// <param name="onException">发生异常时调用</param>
public static async void SafeForget(this Task task, ILogger? logger = null, Action? onCanceled = null, Action<System.Exception>? onException = null)
public static async void SafeForget(this Task task, ILogger? logger = null, Action? onCanceled = null, Action<Exception>? onException = null)
{
try
{
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
onCanceled?.Invoke();
}
catch (System.Exception e)
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
onException?.Invoke(e);

View File

@@ -30,13 +30,13 @@ public readonly struct ThreadPoolSwitchOperation : IAwaitable<ThreadPoolSwitchOp
/// <inheritdoc/>
public void OnCompleted(Action continuation)
{
QueueContinuation(continuation, flowContext: true);
QueueContinuation(continuation, true);
}
/// <inheritdoc/>
public void UnsafeOnCompleted(Action continuation)
{
QueueContinuation(continuation, flowContext: false);
QueueContinuation(continuation, false);
}
private static void QueueContinuation(Action continuation, bool flowContext)

View File

@@ -44,7 +44,7 @@ public static class Must
/// <param name="context">上下文</param>
/// <returns>Nothing. This method always throws.</returns>
[DoesNotReturn]
public static System.Exception NeverHappen(string? context = null)
public static Exception NeverHappen(string? context = null)
{
throw new NotSupportedException(context);
}
@@ -62,21 +62,4 @@ public static class Must
{
return value ?? throw new ArgumentNullException(parameterName);
}
/// <summary>
/// Throws an <see cref="ArgumentNullException"/> if the specified parameter's value is IntPtr.Zero.
/// </summary>
/// <param name="value">The value of the argument.</param>
/// <param name="parameterName">The name of the parameter to include in any thrown exception.</param>
/// <returns>The value of the parameter.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is <see cref="IntPtr.Zero"/>.</exception>
public static Windows.Win32.Foundation.HWND NotNull(Windows.Win32.Foundation.HWND value, [CallerArgumentExpression("value")] string? parameterName = null)
{
if (value == default)
{
throw new ArgumentNullException(parameterName);
}
return value;
}
}

View File

@@ -16,7 +16,7 @@ internal abstract class WebView2Helper
{
private static bool hasEverDetected;
private static bool isSupported;
private static string version = "未检测到 WebView2 运行时";
private static string version = SH.CoreWebView2HelperVersionUndetected;
/// <summary>
/// 检测 WebView2 是否存在
@@ -36,7 +36,7 @@ internal abstract class WebView2Helper
catch (FileNotFoundException ex)
{
ILogger<WebView2Helper> logger = Ioc.Default.GetRequiredService<ILogger<WebView2Helper>>();
logger.LogError(EventIds.WebView2EnvironmentException, ex, "WebView2 运行时未安装");
logger.LogError(EventIds.WebView2EnvironmentException, ex, "WebView2 Runtime not installed.");
isSupported = false;
}
}

View File

@@ -124,8 +124,8 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
private void InitializeWindow()
{
appWindow.Title = "胡桃";
appWindow.SetIcon(Path.Combine(Package.Current.InstalledLocation.Path, "Assets/Logos/Logo.ico"));
appWindow.Title = string.Format(SH.AppNameAndVersion, CoreEnvironment.Version);
appWindow.SetIcon(Path.Combine(CoreEnvironment.InstalledLocation, "Assets/Logo.ico"));
ExtendsContentIntoTitleBar();
Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize);

View File

@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Theme;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
@@ -140,7 +141,7 @@ public class SystemBackdrop
/// <summary>
/// 确保系统调度队列控制器存在
/// </summary>
public void Ensure()
public unsafe void Ensure()
{
if (DispatcherQueue.GetForCurrentThread() != null)
{
@@ -152,7 +153,7 @@ public class SystemBackdrop
{
DispatcherQueueOptions options = new()
{
DwSize = Marshal.SizeOf<DispatcherQueueOptions>(),
DwSize = sizeof(DispatcherQueueOptions),
ThreadType = 2, // DQTYPE_THREAD_CURRENT
ApartmentType = 2, // DQTAT_COM_STA
};

View File

@@ -37,7 +37,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar)
{
this.window = window;
this.hwnd = Must.NotNull(hwnd);
this.hwnd = hwnd;
this.isLegacyDragBar = isLegacyDragBar;
}
@@ -45,7 +45,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
/// 尝试设置窗体子类
/// </summary>
/// <returns>是否设置成功</returns>
public bool TrySetWindowSubclass()
public unsafe bool TrySetWindowSubclass()
{
windowProc = new(OnSubclassProcedure);
bool windowHooked = SetWindowSubclass(hwnd, windowProc, WindowSubclassId, 0);
@@ -115,4 +115,4 @@ internal class WindowSubclassManager<TWindow> : IDisposable
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}
}
}

View File

@@ -8,22 +8,6 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static class DateTimeOffsetExtension
{
/// <summary>
/// Converts the current <see cref="DateTimeOffset"/> to a <see cref="DateTimeOffset"/> that represents the local time.
/// </summary>
/// <param name="dateTimeOffset">时间偏移</param>
/// <param name="keepTicks">保留主时间部分</param>
/// <returns>A <see cref="DateTimeOffset"/> that represents the local time.</returns>
public static DateTimeOffset ToLocalTime(this DateTimeOffset dateTimeOffset, bool keepTicks)
{
if (keepTicks)
{
dateTimeOffset -= TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now);
}
return dateTimeOffset.ToLocalTime();
}
/// <summary>
/// 从Unix时间戳转换
/// </summary>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Annotation;
using System.Reflection;
namespace Snap.Hutao.Extension;
@@ -39,4 +40,25 @@ public static class EnumExtension
DescriptionAttribute? attr = field?.GetCustomAttribute<DescriptionAttribute>();
return attr?.Description;
}
/// <summary>
/// 获取本地化的描述
/// </summary>
/// <typeparam name="TEnum">枚举的类型</typeparam>
/// <param name="enum">枚举值</param>
/// <returns>本地化的描述</returns>
public static string GetLocalizedDescription<TEnum>(this TEnum @enum)
where TEnum : struct, Enum
{
string enumName = Enum.GetName(@enum)!;
FieldInfo? field = @enum.GetType().GetField(enumName);
LocalizationKeyAttribute? attr = field?.GetCustomAttribute<LocalizationKeyAttribute>();
string? result = null;
if (attr != null)
{
result = SH.ResourceManager.GetString(attr.Key);
}
return result ?? enumName;
}
}

View File

@@ -1,18 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
/// <summary>
/// 日志器扩展
/// </summary>
[SuppressMessage("", "CA2254")]
public static class LoggerExtension
{
/// <inheritdoc cref="LoggerExtensions.LogWarning(ILogger, string?, object?[])"/>
public static T LogWarning<T>(this ILogger logger, string message, params object?[] param)
{
logger.LogWarning(message, param);
return default!;
}
}

View File

@@ -33,17 +33,4 @@ public static class StringBuilderExtensions
{
return condition ? sb.Append(value) : sb;
}
/// <summary>
/// 当条件符合时执行 <see cref="StringBuilder.Append(string?)"/>
/// </summary>
/// <param name="sb">字符串建造器</param>
/// <param name="condition">条件</param>
/// <param name="trueValue">条件符合时附加的字符串</param>
/// <param name="falseValue">条件不符合时附加的字符串</param>
/// <returns>同一个字符串建造器</returns>
public static StringBuilder AppendIfElse(this StringBuilder sb, bool condition, string? trueValue, string? falseValue)
{
return condition ? sb.Append(trueValue) : sb.Append(falseValue);
}
}

View File

@@ -60,7 +60,7 @@ internal class ContentDialogFactory : IContentDialogFactory
Title = title,
Content = content,
DefaultButton = ContentDialogButton.Primary,
PrimaryButtonText = "确认",
PrimaryButtonText = SH.ContentDialogConfirmPrimaryButtonText,
};
return dialog;
@@ -75,8 +75,8 @@ internal class ContentDialogFactory : IContentDialogFactory
Title = title,
Content = content,
DefaultButton = defaultButton,
PrimaryButtonText = "确认",
CloseButtonText = "取消",
PrimaryButtonText = SH.ContentDialogConfirmPrimaryButtonText,
CloseButtonText = SH.ContentDialogCancelCloseButtonText,
};
return dialog;

View File

@@ -13,6 +13,7 @@ global using Snap.Hutao.Core.DependencyInjection;
global using Snap.Hutao.Core.DependencyInjection.Annotation;
global using Snap.Hutao.Core.Threading;
global using Snap.Hutao.Core.Validation;
global using Snap.Hutao.Resource.Localization;
// Runtime
global using System;

View File

@@ -1,52 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
[Migration("20220720121521_Logs")]
partial class Logs
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,35 +0,0 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
public partial class Logs : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "logs",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
Category = table.Column<string>(type: "TEXT", nullable: false),
LogLevel = table.Column<int>(type: "INTEGER", nullable: false),
EventId = table.Column<int>(type: "INTEGER", nullable: false),
Message = table.Column<string>(type: "TEXT", nullable: false),
Exception = table.Column<string>(type: "TEXT", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_logs", x => x.InnerId);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "logs");
}
}
}

View File

@@ -1,55 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
[Migration("20220903071033_LogTime")]
partial class LogTime
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.8");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,27 +0,0 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
public partial class LogTime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "Time",
table: "logs",
type: "TEXT",
nullable: false,
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Time",
table: "logs");
}
}
}

View File

@@ -1,53 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
partial class LogDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.8");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -49,6 +49,6 @@ public abstract class WishBase
/// </summary>
public string TotalCountFormatted
{
get => $"{TotalCount} 抽";
get => string.Format(SH.ModelBingGachaWishBaseTotalCountFormat, TotalCount);
}
}

View File

@@ -21,7 +21,7 @@ public class GachaStatistics
/// <summary>
/// 奔行世间
/// </summary>
public TypedWishSummary PermanentWish { get; set; } = default!;
public TypedWishSummary StandardWish { get; set; } = default!;
/// <summary>
/// 历史卡池

View File

@@ -20,7 +20,7 @@ public class TypedWishSummary : WishBase
/// </summary>
public string MaxOrangePullFormatted
{
get => $"最非 {MaxOrangePull} 抽";
get => string.Format(SH.ModelBingGachaTypedWishSummaryMaxOrangePullFormat, MaxOrangePull);
}
/// <summary>
@@ -33,7 +33,7 @@ public class TypedWishSummary : WishBase
/// </summary>
public string MinOrangePullFormatted
{
get => $"最欧 {MinOrangePull} 抽";
get => string.Format(SH.ModelBingGachaTypedWishSummaryMinOrangePullFormat, MinOrangePull);
}
/// <summary>
@@ -46,7 +46,7 @@ public class TypedWishSummary : WishBase
/// </summary>
public string LastOrangePullFormatted
{
get => $"已垫 {LastOrangePull} 抽";
get => string.Format(SH.ModelBingGachaTypedWishSummaryLastPullFormat, LastOrangePull);
}
/// <summary>
@@ -64,7 +64,7 @@ public class TypedWishSummary : WishBase
/// </summary>
public string LastPurplePullFormatted
{
get => $"已垫 {LastPurplePull} 抽";
get => string.Format(SH.ModelBingGachaTypedWishSummaryLastPullFormat, LastPurplePull);
}
/// <summary>
@@ -136,7 +136,7 @@ public class TypedWishSummary : WishBase
/// </summary>
public string AverageOrangePullFormatted
{
get => $"{AverageOrangePull:f2} 抽";
get => string.Format(SH.ModelBingGachaTypedWishSummaryAveragePullFormat, AverageOrangePull);
}
/// <summary>
@@ -149,7 +149,7 @@ public class TypedWishSummary : WishBase
/// </summary>
public string AverageUpOrangePullFormatted
{
get => $"{AverageUpOrangePull:f2} 抽";
get => string.Format(SH.ModelBingGachaTypedWishSummaryAveragePullFormat, AverageUpOrangePull);
}
/// <summary>

View File

@@ -15,9 +15,9 @@ public class LaunchScheme
/// </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"),
new LaunchScheme(SH.ModelBindingLaunchGameLaunchSchemeChinese, "eYd89JmJ", "18", "1", "1"),
new LaunchScheme(SH.ModelBindingLaunchGameLaunchSchemeBilibili, "KAtdSsoQ", "17", "14", "0"),
new LaunchScheme(SH.ModelBindingLaunchGameLaunchSchemeOversea, "gcStgarh", "10", "1", "0"),
}.ToImmutableList();
/// <summary>

View File

@@ -1,39 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Logging;
namespace Snap.Hutao.Model.Entity.Database;
/// <summary>
/// 日志数据库上下文
/// 由于写入日志的行为需要锁定数据库上下文
/// 所以将日志单独分离出来进行读写
/// </summary>
public class LogDbContext : DbContext
{
/// <summary>
/// 创建一个新的
/// </summary>
/// <param name="options">选项</param>
private LogDbContext(DbContextOptions<LogDbContext> options)
: base(options)
{
}
/// <summary>
/// 日志记录
/// </summary>
public DbSet<LogEntry> Logs { get; set; } = default!;
/// <summary>
/// 构造一个临时的日志数据库上下文
/// </summary>
/// <param name="sqlConnectionString">连接字符串</param>
/// <returns>日志数据库上下文</returns>
public static LogDbContext Create(string sqlConnectionString)
{
return new(new DbContextOptionsBuilder<LogDbContext>().UseSqlite(sqlConnectionString).Options);
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Design;
using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Context.Database;
/// <summary>
/// 此类只用于在生成迁移时提供数据库上下文
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public class LogDbContextDesignTimeFactory : IDesignTimeDbContextFactory<LogDbContext>
{
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public LogDbContext CreateDbContext(string[] args)
{
string logDbName = System.IO.Path.Combine(Core.CoreEnvironment.DataFolder, "Log.db");
return LogDbContext.Create($"Data Source={logDbName}");
}
}

View File

@@ -40,7 +40,6 @@ public class GameAccount : INotifyPropertyChanged
/// <summary>
/// [MIHOYOSDK_ADL_PROD_CN_h3123967166]
/// see <see cref="Service.Game.GameAccountRegistryInterop.SdkKey"/>
/// </summary>
public string MihoyoSDK { get; set; } = default!;

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

@@ -41,4 +41,21 @@ public class UIGF
{
return SupportedVersion.Contains(Info?.UIGFVersion ?? string.Empty);
}
/// <summary>
/// 列表物品是否正常
/// </summary>
/// <returns>是否正常</returns>
public bool IsValidList()
{
foreach (UIGFItem item in List)
{
if (item.ItemType != "角色" || item.ItemType != "武器")
{
return false;
}
}
return true;
}
}

View File

@@ -4,7 +4,7 @@ WM_GETMINMAXINFO
WM_NCRBUTTONDOWN
WM_NCRBUTTONUP
// Type definition
// Type & Enum definition
CWMO_FLAGS
MINMAXINFO

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<StoreAssociation xmlns="http://schemas.microsoft.com/appx/2010/storeassociation">
<Publisher>CN=35C8E923-85DF-49A7-9172-B39DC6312C52</Publisher>
<PublisherDisplayName>DGP Studio</PublisherDisplayName>
<DeveloperAccountType>MSA</DeveloperAccountType>
<GeneratePackageHash>http://www.w3.org/2001/04/xmlenc#sha256</GeneratePackageHash>
<SupportedLocales>
<Language Code="en-us" InMinimumRequirementSet="true" />
<Language Code="zh-cn" InMinimumRequirementSet="true" />
</SupportedLocales>
<ProductReservedInfo>
<MainPackageIdentityName>60568DGPStudio.SnapHutao</MainPackageIdentityName>
<ReservedNames>
<ReservedName>Snap Hutao</ReservedName>
<ReservedName>胡桃工具箱</ReservedName>
</ReservedNames>
</ProductReservedInfo>
<AccountPackageIdentityNames>
<MainPackageIdentityName>60568DGPStudio.SnapGenshinResin</MainPackageIdentityName>
</AccountPackageIdentityNames>
<PackageInfoList LandingUrl="https://developer.microsoft.com/dashboard/Application?appId=9PH4NXJ2JN52" />
</StoreAssociation>

View File

@@ -4,31 +4,29 @@
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
IgnorableNamespaces="com uap desktop desktop6 rescap">
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
IgnorableNamespaces="com uap desktop rescap mp">
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio&amp;comma DissmissedLight"
Version="1.3.13.0" />
Name="60568DGPStudio.SnapHutao"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.4.11.0" />
<Properties>
<DisplayName>胡桃</DisplayName>
<DisplayName>Snap Hutao</DisplayName>
<PublisherDisplayName>DGP Studio</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
</Properties>
<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.22621.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>
<Resource Language="zh-CN"/>
<Resource Language="en-us"/>
<Resource Language="zh-cn"/>
</Resources>
<Applications>
@@ -36,7 +34,7 @@
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="胡桃"
DisplayName="Snap Hutao"
Description="A Snap Hutao Software"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
@@ -66,6 +64,5 @@
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
<rescap:Capability Name="unvirtualizedResources"/>
</Capabilities>
</Package>

View File

@@ -5,7 +5,6 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Logging;
using System.Runtime.InteropServices;
using WinRT;
@@ -33,7 +32,7 @@ public static partial class Program
// In a Desktop app this runs a message pump internally,
// and does not return until the application shuts down.
Application.Start(InitializeApp);
ServiceScopeExtension.DisposeLast();
Control.ScopedPage.DisposePreviousScope();
}
AppInstance.GetCurrent().UnregisterKey();
@@ -54,11 +53,11 @@ public static partial class Program
ServiceProvider services = new ServiceCollection()
// Microsoft extension
.AddLogging(builder => builder.AddDebug().AddDatabase())
.AddLogging(builder => builder.AddDebug())
.AddMemoryCache()
// Hutao extensions
.AddJsonSerializerOptions()
.AddJsonOptions()
.AddDatebase()
.AddInjections()
.AddHttpClients()
@@ -66,7 +65,7 @@ public static partial class Program
// Discrete services
.AddSingleton<IMessenger>(WeakReferenceMessenger.Default)
.BuildServiceProvider();
.BuildServiceProvider(true);
Ioc.Default.ConfigureServices(services);
return services;

View File

@@ -3,7 +3,8 @@
"Snap.Hutao (Package)": {
"commandName": "MsixPackage",
"nativeDebugging": false,
"doNotLaunchApp": false
"doNotLaunchApp": false,
"allowLocalNetworkLoopbackProperty": true
},
"Snap.Hutao (Unpackaged)": {
"commandName": "Project"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -72,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);
}
@@ -115,14 +114,14 @@ internal class AchievementService : IAchievementService
List<BindingAchievement> results = new();
foreach (MetadataAchievement meta in metadata)
{
EntityAchievement? entity;
EntityAchievement? entity = null;
try
{
entity = entities.SingleOrDefault(e => e.Id == meta.Id);
}
catch (InvalidOperationException ex)
{
throw new UserdataCorruptedException("单个成就存档内发现多个相同的成就 Id", ex);
ThrowHelper.UserdataCorrupted(SH.ServiceAchievementUserdataCorruptedInnerIdNotUnique, ex);
}
entity ??= EntityAchievement.Create(archiveId, meta.Id);
@@ -155,7 +154,6 @@ internal class AchievementService : IAchievementService
/// <inheritdoc/>
public async Task<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategy strategy)
{
// return Task.Run(() => ImportFromUIAF(archive, list, strategy));
await ThreadHelper.SwitchToBackgroundAsync();
Guid archiveId = archive.InnerId;
@@ -191,7 +189,7 @@ internal class AchievementService : IAchievementService
public void SaveAchievements(EntityArchive archive, IList<BindingAchievement> achievements)
{
string name = archive.Name;
logger.LogInformation(EventIds.Achievement, "Begin saving achievements for [{name}]", name);
logger.LogInformation("Begin saving achievements for [{name}]", name);
ValueStopwatch stopwatch = ValueStopwatch.StartNew();
IEnumerable<EntityAchievement> newData = achievements
@@ -201,8 +199,8 @@ internal class AchievementService : IAchievementService
ImportResult result = achievementDbOperation.Overwrite(archive.InnerId, newData);
double time = stopwatch.GetElapsedTime().TotalMilliseconds;
logger.LogInformation(EventIds.Achievement, "{add} added, {update} updated, {remove} removed", result.Add, result.Update, result.Remove);
logger.LogInformation(EventIds.Achievement, "Save achievements for [{name}] completed in {time}ms", name, time);
logger.LogInformation("{add} added, {update} updated, {remove} removed", result.Add, result.Update, result.Remove);
logger.LogInformation("Save achievements for [{name}] completed in {time}ms", name, time);
}
/// <inheritdoc/>

View File

@@ -57,7 +57,7 @@ internal partial class AnnouncementService : IAnnouncementService
Dictionary<int, string> contentMap = contents
.ToDictionary(id => id.AnnId, content => content.Content);
// 将活动公告置于
// 将活动公告置于
wrapper.List.Reverse();
// 将公告内容联入公告列表

View File

@@ -60,7 +60,7 @@ internal class SummaryAvatarFactory
FetterLevel = avatarInfo.FetterInfo?.ExpLevel ?? 0,
Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap),
CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}",
LevelNumber = int.Parse(avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value ?? string.Empty),
LevelNumber = avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].ValueInt32 ?? 0,
// processed webinfo part
Weapon = reliquaryAndWeapon.Weapon,

View File

@@ -35,11 +35,10 @@ internal class SummaryFactoryImplementation
return new()
{
Player = SummaryHelper.CreatePlayer(playerInfo),
Avatars = avatarInfos.Where(a => !AvatarIds.IsPlayer(a.AvatarId)).Select(a =>
{
SummaryAvatarFactory summaryAvatarFactory = new(metadataContext, a);
return summaryAvatarFactory.CreateAvatar();
}).ToList(),
Avatars = avatarInfos
.Where(a => !AvatarIds.IsPlayer(a.AvatarId))
.Select(a => new SummaryAvatarFactory(metadataContext, a).CreateAvatar())
.ToList(),
};
}
}

View File

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

View File

@@ -54,10 +54,10 @@ internal class DailyNoteNotifier
if (!entry.ResinNotifySuppressed)
{
notifyInfos.Add(new(
"原粹树脂",
SH.ServiceDailyNoteNotifierResin,
"ms-appx:///Resource/Icon/UI_ItemIcon_210_256.png",
$"{entry.DailyNote.CurrentResin}",
$"当前原粹树脂:{entry.DailyNote.CurrentResin}"));
string.Format(SH.ServiceDailyNoteNotifierResinCurrent, entry.DailyNote.CurrentResin)));
entry.ResinNotifySuppressed = true;
}
}
@@ -71,10 +71,10 @@ internal class DailyNoteNotifier
if (!entry.HomeCoinNotifySuppressed)
{
notifyInfos.Add(new(
"洞天宝钱",
SH.ServiceDailyNoteNotifierHomeCoin,
"ms-appx:///Resource/Icon/UI_ItemIcon_204.png",
$"{entry.DailyNote.CurrentHomeCoin}",
$"当前洞天宝钱:{entry.DailyNote.CurrentHomeCoin}"));
string.Format(SH.ServiceDailyNoteNotifierHomeCoinCurrent, entry.DailyNote.CurrentHomeCoin)));
entry.HomeCoinNotifySuppressed = true;
}
}
@@ -88,9 +88,9 @@ internal class DailyNoteNotifier
if (!entry.DailyTaskNotifySuppressed)
{
notifyInfos.Add(new(
"每日委托",
SH.ServiceDailyNoteNotifierDailyTask,
"ms-appx:///Resource/Icon/UI_MarkQuest_Events_Proce.png",
$"奖励待领取",
SH.ServiceDailyNoteNotifierDailyTaskHint,
entry.DailyNote.ExtraTaskRewardDescription));
entry.DailyTaskNotifySuppressed = true;
}
@@ -105,10 +105,10 @@ internal class DailyNoteNotifier
if (!entry.TransformerNotifySuppressed)
{
notifyInfos.Add(new(
"参量质变仪",
SH.ServiceDailyNoteNotifierTransformer,
"ms-appx:///Resource/Icon/UI_ItemIcon_220021.png",
$"准备完成",
"参量质变仪已准备完成"));
SH.ServiceDailyNoteNotifierTransformerAdaptiveHint,
SH.ServiceDailyNoteNotifierTransformerHint));
entry.TransformerNotifySuppressed = true;
}
}
@@ -122,10 +122,10 @@ internal class DailyNoteNotifier
if (!entry.ExpeditionNotifySuppressed)
{
notifyInfos.Add(new(
"探索派遣",
AvatarIconConverter.IconNameToUri("UI_AvatarIcon_Side_None.png").ToString(),
$"已完成",
"探索派遣已完成"));
SH.ServiceDailyNoteNotifierExpedition,
Web.HutaoEndpoints.UIAvatarIconSideNone.ToString(), // TODO: embed this
SH.ServiceDailyNoteNotifierExpeditionAdaptiveHint,
SH.ServiceDailyNoteNotifierExpeditionHint));
entry.ExpeditionNotifySuppressed = true;
}
}
@@ -150,7 +150,7 @@ internal class DailyNoteNotifier
.GetActionTicketByStokenAsync("game_role", entry.User)
.ConfigureAwait(false);
string? attribution = "请求异常";
string? attribution = SH.ServiceDailyNoteNotifierAttribution;
if (actionTicketResponse.IsOk())
{
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
@@ -166,10 +166,13 @@ internal class DailyNoteNotifier
}
ToastContentBuilder builder = new ToastContentBuilder()
.AddHeader("DAILYNOTE", "实时便笺提醒", "DAILYNOTE")
.AddHeader("DAILYNOTE", SH.ServiceDailyNoteNotifierTitle, "DAILYNOTE")
.AddAttributionText(attribution)
.AddButton(new ToastButton().SetContent("开始游戏").AddArgument("Action", "LaunchGame").AddArgument("Uid", entry.Uid))
.AddButton(new ToastButtonDismiss("我知道了"));
.AddButton(new ToastButton()
.SetContent(SH.ServiceDailyNoteNotifierActionLaunchGameButton)
.AddArgument("Action", Core.LifeCycle.Activation.LaunchGame)
.AddArgument("Uid", entry.Uid))
.AddButton(new ToastButtonDismiss(SH.ServiceDailyNoteNotifierActionLaunchGameDismiss));
if (appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteReminderNotify, SettingEntryHelper.FalseString).GetBoolean())
{
@@ -178,7 +181,7 @@ internal class DailyNoteNotifier
if (notifyInfos.Count > 2)
{
builder.AddText("多个提醒项达到设定值");
builder.AddText(SH.ServiceDailyNoteNotifierMultiValueReached);
// Desktop and Mobile started supporting adaptive toasts in API contract 3 (Anniversary Update)
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 3))

View File

@@ -157,14 +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())
{
// DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s)
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

@@ -17,7 +17,7 @@ public class GachaConfigTypeComparar : IComparer<GachaConfigType>
{ GachaConfigType.AvatarEventWish, 0 },
{ GachaConfigType.AvatarEventWish2, 1 },
{ GachaConfigType.WeaponEventWish, 2 },
{ GachaConfigType.PermanentWish, 3 },
{ GachaConfigType.StandardWish, 3 },
{ GachaConfigType.NoviceWish, 4 },
}.ToImmutableDictionary();

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Entity;
@@ -65,9 +66,9 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
Dictionary<WeaponId, Weapon> weaponMap,
bool isEmptyHistoryWishVisible)
{
TypedWishSummaryBuilder permanentWishBuilder = new("奔行世间", TypedWishSummaryBuilder.IsPermanentWish, 90, 10);
TypedWishSummaryBuilder avatarWishBuilder = new("角色活动", TypedWishSummaryBuilder.IsAvatarEventWish, 90, 10);
TypedWishSummaryBuilder weaponWishBuilder = new("神铸赋形", TypedWishSummaryBuilder.IsWeaponEventWish, 80, 10);
TypedWishSummaryBuilder standardWishBuilder = new(SH.ServiceGachaLogFactoryPermanentWishName, TypedWishSummaryBuilder.IsPermanentWish, 90, 10);
TypedWishSummaryBuilder avatarWishBuilder = new(SH.ServiceGachaLogFactoryAvatarWishName, TypedWishSummaryBuilder.IsAvatarEventWish, 90, 10);
TypedWishSummaryBuilder weaponWishBuilder = new(SH.ServiceGachaLogFactoryWeaponWishName, TypedWishSummaryBuilder.IsWeaponEventWish, 80, 10);
Dictionary<Avatar, int> orangeAvatarCounter = new();
Dictionary<Avatar, int> purpleAvatarCounter = new();
@@ -102,7 +103,7 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
break;
}
permanentWishBuilder.Track(item, avatar, isUp);
standardWishBuilder.Track(item, avatar, isUp);
avatarWishBuilder.Track(item, avatar, isUp);
weaponWishBuilder.Track(item, avatar, isUp);
}
@@ -129,14 +130,15 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
break;
}
permanentWishBuilder.Track(item, weapon, isUp);
standardWishBuilder.Track(item, weapon, isUp);
avatarWishBuilder.Track(item, weapon, isUp);
weaponWishBuilder.Track(item, weapon, isUp);
}
else
{
// ItemId place not correct.
Must.NeverHappen();
// TODO: check items id when importing
ThrowHelper.UserdataCorrupted(string.Format(SH.ServiceGachaStatisticsFactoryItemIdInvalid, item.ItemId), null!);
}
}
@@ -160,7 +162,7 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
BlueWeapons = blueWeaponCounter.ToStatisticsList(),
// typed wish summary
PermanentWish = permanentWishBuilder.ToTypedWishSummary(),
StandardWish = standardWishBuilder.ToTypedWishSummary(),
AvatarWish = avatarWishBuilder.ToTypedWishSummary(),
WeaponWish = weaponWishBuilder.ToTypedWishSummary(),
};

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