Compare commits

...

85 Commits

Author SHA1 Message Date
DismissedLight
98c003ae77 Merge branch 'develop' into feat/1239 2024-01-02 13:18:32 +08:00
qhy040404
48774960a7 Update GameRegistryContentTest.cs 2024-01-02 10:20:57 +08:00
DismissedLight
7bfea0e090 Create GameRegistryContentTest.cs 2024-01-01 23:21:38 +08:00
qhy040404
d26611ccf7 impl #1239 2024-01-01 20:13:11 +08:00
qhy040404
f0f9e387a8 direct to right doc 2024-01-01 19:35:01 +08:00
DismissedLight
f71a34a6be Merge pull request #1243 from DGP-Studio/fix/1208
fix #1208
2024-01-01 00:13:59 +08:00
DismissedLight
e6fd0b833b fix 1203 status deserialize 2023-12-31 23:59:55 +08:00
DismissedLight
d2c33cf19c optimize cache image placeholder presentation 2023-12-31 23:50:01 +08:00
qhy040404
59a7d6746f fix #1208 2023-12-31 23:36:29 +08:00
Lightczx
b49cd924d0 add source link 2023-12-29 13:51:27 +08:00
Lightczx
49db3003c9 fix launch game window 2023-12-29 11:56:44 +08:00
Lightczx
314c771020 clear selected game account after scheme changed 2023-12-29 11:39:01 +08:00
Lightczx
967f6f76f0 refuse convert for game in Program Files folder 2023-12-29 11:11:53 +08:00
Lightczx
5d05c31af5 fix startup crash 2023-12-29 10:05:03 +08:00
Lightczx
64998453a1 Update LaunchGameViewModel.cs 2023-12-28 17:07:15 +08:00
Lightczx
9fdedd78d0 refactor Launch Game Pipeline 2023-12-28 17:06:45 +08:00
Lightczx
58e4d1b90e fix ci 2023-12-28 10:30:10 +08:00
Lightczx
e0d11bf9a0 impl #1199 2023-12-28 10:13:41 +08:00
Lightczx
51be2c76aa remove unused strings 2023-12-27 13:44:20 +08:00
DismissedLight
686d2378de Merge pull request #1232 from DGP-Studio/feat/elevate_restart 2023-12-27 13:34:48 +08:00
Lightczx
e2d5baffe0 remove INotifyPropertyChanged on TitleView 2023-12-27 13:33:01 +08:00
Lightczx
4001cc7051 code style 2023-12-27 13:31:21 +08:00
qhy040404
b106fe4729 add restart as admin 2023-12-27 10:44:10 +08:00
DismissedLight
d138d856e4 prepare 1203 types 2023-12-26 22:46:50 +08:00
DismissedLight
91f16c1701 impl #1230 2023-12-26 22:10:57 +08:00
DismissedLight
54d21b24f7 use package manager to update 2023-12-26 21:34:42 +08:00
Lightczx
268c2d0543 Update Snap.Hutao.csproj 2023-12-26 11:47:02 +08:00
Lightczx
acdcee7558 fix ci 2023-12-26 10:42:30 +08:00
Lightczx
371e469db7 optimize progress invocation 2023-12-26 10:36:59 +08:00
DismissedLight
22a974408d Merge pull request #1227 from DGP-Studio/feat/hotkey_flyout 2023-12-25 19:43:23 +08:00
DismissedLight
055b343571 fixup 2023-12-25 19:40:43 +08:00
qhy040404
84e56792b0 use flyout to show special keyboard keys 2023-12-25 19:26:59 +08:00
DismissedLight
da95b7837a Merge pull request #1218 from DGP-Studio/feat/goodbye_pwsh 2023-12-24 21:51:11 +08:00
DismissedLight
48ddb4c091 code style 2023-12-24 21:50:47 +08:00
qhy040404
ea95f2e2b1 say goodbye to powershell 2023-12-24 17:09:49 +08:00
DismissedLight
93077104b8 direct set registry value 2023-12-24 13:52:06 +08:00
DismissedLight
3ffdc901c7 fix server convert set game path null 2023-12-24 12:52:06 +08:00
DismissedLight
0d66c85744 remove redundant element 2023-12-23 20:42:35 +08:00
DismissedLight
d293149672 1.9.1 package 2023-12-23 19:18:29 +08:00
DismissedLight
3784df67a3 adjust launch page ui 2023-12-23 19:15:04 +08:00
DismissedLight
4aaca4d19f fix reentrant issue 2023-12-23 18:51:41 +08:00
DismissedLight
e6cf39831d fix daily note fetch uid crash 2023-12-23 18:22:12 +08:00
DismissedLight
24a2a18760 fix #1212 2023-12-23 17:34:44 +08:00
DismissedLight
d8dce5c062 empty sha256 tolerance 2023-12-23 14:48:24 +08:00
Masterain
ccbb7f76d4 New Crowdin updates (#1205) 2023-12-23 11:48:39 +08:00
DismissedLight
857eea61f9 remove store buttons in setting page 2023-12-23 11:47:15 +08:00
DismissedLight
d82f416c10 code style 2023-12-22 22:03:37 +08:00
DismissedLight
b8bcad2107 1.9.0 package 2023-12-22 22:02:10 +08:00
Masterain
ad240a543d New Crowdin updates (#1189)
* New translations sh.resx (Indonesian)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (English)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)
2023-12-22 02:21:02 -08:00
Masterain
e7775b611f Update TestViewModel.cs 2023-12-22 01:12:45 -08:00
Masterain
53d920621c Update TestViewModel.cs 2023-12-22 00:50:00 -08:00
Lightczx
55cb346fb4 update service 2023-12-22 16:29:36 +08:00
Masterain
c0f63187cc Update TestViewModel.cs 2023-12-21 23:28:46 -08:00
DismissedLight
884ec87edf disable quick edit for debug console 2023-12-21 20:16:00 +08:00
Lightczx
18d3180bc2 more announcement time fix 2023-12-21 15:57:09 +08:00
Lightczx
4908364e45 announcement time as local 2023-12-21 15:27:38 +08:00
DismissedLight
b7fe16c52c Merge pull request #1200 from DGP-Studio/1198 2023-12-21 15:22:30 +08:00
Lightczx
0c8646b499 fix announcement time 2023-12-21 15:20:22 +08:00
qhy040404
f5b0d07d32 impl #1198 2023-12-21 10:06:30 +08:00
qhy040404
231635ac89 fix wrong publisher 2023-12-21 09:42:18 +08:00
qhy040404
e0a28d0f90 Update CI Certificate 2023-12-21 09:34:38 +08:00
Lightczx
22e7942899 doc 2023-12-20 17:01:45 +08:00
Lightczx
d81e7f6624 fix announcement time incorrect for oversea 2023-12-20 16:57:07 +08:00
DismissedLight
92240a27a0 Merge pull request #1192 from DGP-Studio/feat/ann 2023-12-20 16:30:37 +08:00
Lightczx
c5313c078d code style 2023-12-20 16:29:00 +08:00
qhy040404
2c320fe7e6 revert some region 2023-12-20 15:49:18 +08:00
qhy040404
05a8ab990c replace all region 2023-12-20 15:27:59 +08:00
qhy040404
3661822852 use NameValue 2023-12-20 15:27:59 +08:00
qhy040404
7519d7b263 typo 2023-12-20 15:27:59 +08:00
qhy040404
47d0cbcf31 override ToString 2023-12-20 15:27:59 +08:00
qhy040404
449a5393a9 fix typo 2023-12-20 15:27:59 +08:00
qhy040404
3b636ecd27 Update SettingPage.xaml 2023-12-20 15:27:59 +08:00
qhy040404
95531db559 use struct 2023-12-20 15:27:59 +08:00
qhy040404
eeed58ed71 maybe code style 2023-12-20 15:27:58 +08:00
qhy040404
493af0fd4c impl #1112 (part 3)
ann client
2023-12-20 15:27:58 +08:00
qhy040404
3df70a5feb impl #1112 (part 2)
setting
2023-12-20 15:27:58 +08:00
qhy040404
879b930ea6 impl #1112 (part 1) 2023-12-20 15:27:58 +08:00
Lightczx
c5e0221a0b fix jsbridge 2023-12-20 15:26:08 +08:00
Lightczx
44fbb56d83 minor code style 2023-12-20 13:07:06 +08:00
Lightczx
1a1bdb7f85 #1190 cast data type nuint attempt 2023-12-20 12:43:08 +08:00
Lightczx
52cd505ed0 #1190 cast data type 2023-12-20 11:01:01 +08:00
Lightczx
cd16bebee2 fix #1190 2023-12-20 10:39:56 +08:00
DismissedLight
2be2d6313b wiki avatar skill 2023-12-19 20:37:27 +08:00
Lightczx
bee7e48cb9 fix gamePath set null when closing page 2 2023-12-19 11:49:19 +08:00
DismissedLight
83cbc9bbe1 fix gamePath set null when closing page 2023-12-18 22:44:31 +08:00
142 changed files with 8293 additions and 1415 deletions

View File

@@ -57,7 +57,9 @@ jobs:
> 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
> [!IMPORTANT]
> 请安装 [Snap.Hutao.CI.cer](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate/Snap.Hutao.CI.cer) 以安装测试版安装包
> 请注意,从 Snap Hutao Alpha 2023.12.21.3 开始,我们将使用全新的 CI 证书,原有的 Snap.Hutao.CI.cer 将在几天后过期停止使用。
>
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 以安装测试版安装包
"
echo $summary >> $Env:GITHUB_STEP_SUMMARY
echo $summary >> $Env:GITHUB_STEP_SUMMARY

View File

@@ -114,7 +114,7 @@ Task("Generate AppxManifest")
.Replace("胡桃", "胡桃 Alpha")
.Replace("DGP Studio", "DGP Studio CI");
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"7f0db578-026f-4e0b-a75b-d5d06bb0a74c\"");
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=DGP Studio CI\"");
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"E=admin@dgp-studio.cn, CN=DGP Studio CI, OU=CI, O=DGP-Studio, L=San Jose, S=CA, C=US\"");
content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\"");
}
else if (AppVeyor.IsRunningOnAppVeyor)

View File

@@ -0,0 +1,60 @@
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.Json;
namespace Snap.Hutao.Test.IncomingFeature;
[TestClass]
public class GameRegistryContentTest
{
[TestMethod]
[SupportedOSPlatform("windows")]
public void GetRegistryContent()
{
GetRegistryContentCore(@"Software\miHoYo\原神");
GetRegistryContentCore(@"Software\miHoYo\Genshin Impact");
}
[SupportedOSPlatform("windows")]
private static void GetRegistryContentCore(string subkey)
{
using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64))
{
RegistryKey? gameKey = key.OpenSubKey(subkey);
Assert.IsNotNull(gameKey);
Dictionary<string, object> data = [];
foreach (string valueName in gameKey.GetValueNames())
{
data[valueName] = gameKey.GetValueKind(valueName) switch
{
RegistryValueKind.DWord => (int)gameKey.GetValue(valueName)!,
RegistryValueKind.Binary => GetString((byte[])gameKey.GetValue(valueName)!),
_ => throw new NotImplementedException()
};
}
JsonSerializerOptions options = new()
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
Console.WriteLine($"Subkey: {subkey}");
Console.WriteLine(JsonSerializer.Serialize(data, options));
}
}
private static unsafe string GetString(byte[] bytes)
{
fixed (byte* pByte = bytes)
{
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
return Encoding.UTF8.GetString(span);
}
}
}

View File

@@ -15,12 +15,15 @@ CloseHandle
CreateEventW
CreateRemoteThread
FreeConsole
GetConsoleMode
GetModuleHandleW
GetProcAddress
GetStdHandle
K32EnumProcessModules
K32GetModuleBaseNameW
K32GetModuleInformation
ReadProcessMemory
SetConsoleMode
SetConsoleTitle
SetEvent
VirtualAlloc
@@ -58,6 +61,7 @@ FileSaveDialog
IFileOpenDialog
IFileSaveDialog
IPersistFile
IShellLinkDataList
IShellLinkW
ShellLink
SHELL_LINK_DATA_FLAGS
@@ -66,6 +70,7 @@ SHELL_LINK_DATA_FLAGS
IMemoryBufferByteAccess
// Const value
E_FAIL
INFINITE
RPC_E_WRONG_THREAD
MAX_PATH

View File

@@ -69,12 +69,12 @@ public sealed partial class App : Application
if (firstInstance.IsCurrent)
{
logger.LogInformation(ConsoleBanner);
LogDiagnosticInformation();
// manually invoke
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
activation.InitializeWith(firstInstance);
LogDiagnosticInformation();
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
}
else

View File

@@ -3,7 +3,7 @@
namespace Snap.Hutao.Control;
internal interface IScopedPageScopeReferenceTracker
internal interface IScopedPageScopeReferenceTracker : IDisposable
{
IServiceScope CreateScope();
}

View File

@@ -4,6 +4,8 @@
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Web;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Image;
@@ -40,12 +42,7 @@ internal sealed class CachedImage : Implementation.ImageEx
{
// The image is corrupted, remove it.
imageCache.Remove(imageUri);
return null;
}
catch (OperationCanceledException)
{
// task was explicitly canceled
return null;
return default;
}
}
}

View File

@@ -17,6 +17,7 @@
CornerRadius="{TemplateBinding CornerRadius}">
<Image
Name="PlaceholderImage"
Margin="{TemplateBinding PlaceholderMargin}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Source="{TemplateBinding PlaceholderSource}"

View File

@@ -11,10 +11,6 @@ using Windows.Foundation;
namespace Snap.Hutao.Control.Image.Implementation;
internal delegate void ImageExFailedEventHandler(object sender, ImageExFailedEventArgs e);
internal delegate void ImageExOpenedEventHandler(object sender, ImageExOpenedEventArgs e);
[SuppressMessage("", "CA1001")]
[SuppressMessage("", "SH003")]
[TemplateVisualState(Name = LoadingState, GroupName = CommonGroup)]
@@ -22,98 +18,34 @@ internal delegate void ImageExOpenedEventHandler(object sender, ImageExOpenedEve
[TemplateVisualState(Name = UnloadedState, GroupName = CommonGroup)]
[TemplateVisualState(Name = FailedState, GroupName = CommonGroup)]
[TemplatePart(Name = PartImage, Type = typeof(object))]
internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
[TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))]
[DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("DecodePixelHeight", typeof(int), 0)]
[DependencyProperty("DecodePixelWidth", typeof(int), 0)]
[DependencyProperty("DecodePixelType", typeof(DecodePixelType), DecodePixelType.Physical)]
[DependencyProperty("IsCacheEnabled", typeof(bool), false)]
[DependencyProperty("EnableLazyLoading", typeof(bool), false, nameof(EnableLazyLoadingChanged))]
[DependencyProperty("LazyLoadingThreshold", typeof(double), default(double), nameof(LazyLoadingThresholdChanged))]
[DependencyProperty("PlaceholderSource", typeof(object), default(object))]
[DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("PlaceholderMargin", typeof(Thickness))]
[DependencyProperty("Source", typeof(object), default(object), nameof(SourceChanged))]
internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
{
protected const string PartImage = "Image";
protected const string PartPlaceholderImage = "PlaceholderImage";
protected const string CommonGroup = "CommonStates";
protected const string LoadingState = "Loading";
protected const string LoadedState = "Loaded";
protected const string UnloadedState = "Unloaded";
protected const string FailedState = "Failed";
private static readonly DependencyProperty StretchProperty = DependencyProperty.Register(nameof(Stretch), typeof(Stretch), typeof(ImageExBase), new PropertyMetadata(Stretch.Uniform));
private static readonly DependencyProperty DecodePixelHeightProperty = DependencyProperty.Register(nameof(DecodePixelHeight), typeof(int), typeof(ImageExBase), new PropertyMetadata(0));
private static readonly DependencyProperty DecodePixelTypeProperty = DependencyProperty.Register(nameof(DecodePixelType), typeof(int), typeof(ImageExBase), new PropertyMetadata(DecodePixelType.Physical));
private static readonly DependencyProperty DecodePixelWidthProperty = DependencyProperty.Register(nameof(DecodePixelWidth), typeof(int), typeof(ImageExBase), new PropertyMetadata(0));
private static readonly DependencyProperty IsCacheEnabledProperty = DependencyProperty.Register(nameof(IsCacheEnabled), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false));
private static readonly DependencyProperty EnableLazyLoadingProperty = DependencyProperty.Register(nameof(EnableLazyLoading), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false, EnableLazyLoadingChanged));
private static readonly DependencyProperty LazyLoadingThresholdProperty = DependencyProperty.Register(nameof(LazyLoadingThreshold), typeof(double), typeof(ImageExBase), new PropertyMetadata(default(double), LazyLoadingThresholdChanged));
private static readonly DependencyProperty PlaceholderSourceProperty = DependencyProperty.Register(nameof(PlaceholderSource), typeof(ImageSource), typeof(ImageExBase), new PropertyMetadata(default(ImageSource), PlaceholderSourceChanged));
private static readonly DependencyProperty PlaceholderStretchProperty = DependencyProperty.Register(nameof(PlaceholderStretch), typeof(Stretch), typeof(ImageExBase), new PropertyMetadata(default(Stretch)));
private static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageExBase), new PropertyMetadata(null, SourceChanged));
private CancellationTokenSource? tokenSource;
private object? lazyLoadingSource;
private bool isInViewport;
public event ImageExFailedEventHandler? ImageExFailed;
public event ImageExOpenedEventHandler? ImageExOpened;
public event EventHandler? ImageExInitialized;
public bool IsInitialized { get; private set; }
public int DecodePixelHeight
{
get => (int)GetValue(DecodePixelHeightProperty);
set => SetValue(DecodePixelHeightProperty, value);
}
public DecodePixelType DecodePixelType
{
get => (DecodePixelType)GetValue(DecodePixelTypeProperty);
set => SetValue(DecodePixelTypeProperty, value);
}
public int DecodePixelWidth
{
get => (int)GetValue(DecodePixelWidthProperty);
set => SetValue(DecodePixelWidthProperty, value);
}
public Stretch Stretch
{
get => (Stretch)GetValue(StretchProperty);
set => SetValue(StretchProperty, value);
}
public bool IsCacheEnabled
{
get => (bool)GetValue(IsCacheEnabledProperty);
set => SetValue(IsCacheEnabledProperty, value);
}
public bool EnableLazyLoading
{
get => (bool)GetValue(EnableLazyLoadingProperty);
set => SetValue(EnableLazyLoadingProperty, value);
}
public double LazyLoadingThreshold
{
get => (double)GetValue(LazyLoadingThresholdProperty);
set => SetValue(LazyLoadingThresholdProperty, value);
}
public ImageSource PlaceholderSource
{
get => (ImageSource)GetValue(PlaceholderSourceProperty);
set => SetValue(PlaceholderSourceProperty, value);
}
public Stretch PlaceholderStretch
{
get => (Stretch)GetValue(PlaceholderStretchProperty);
set => SetValue(PlaceholderStretchProperty, value);
}
public object Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
public bool WaitUntilLoaded
{
get => true;
@@ -121,11 +53,9 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
protected object? Image { get; private set; }
public abstract CompositionBrush GetAlphaMask();
protected object? PlaceholderImage { get; private set; }
protected virtual void OnPlaceholderSourceChanged(DependencyPropertyChangedEventArgs e)
{
}
public abstract CompositionBrush GetAlphaMask();
protected virtual Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
@@ -136,61 +66,11 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(this, LoadedState, true);
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
}
protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
{
VisualStateManager.GoToState(this, FailedState, true);
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new FileNotFoundException(e.ErrorMessage)));
}
protected void AttachImageOpened(RoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageOpened += handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageOpened += handler;
}
}
protected void RemoveImageOpened(RoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageOpened -= handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageOpened -= handler;
}
}
protected void AttachImageFailed(ExceptionRoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageFailed += handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageFailed += handler;
}
}
protected void RemoveImageFailed(ExceptionRoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageFailed -= handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageFailed -= handler;
}
}
protected override void OnApplyTemplate()
@@ -199,11 +79,10 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
RemoveImageFailed(OnImageFailed);
Image = GetTemplateChild(PartImage);
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
IsInitialized = true;
ImageExInitialized?.Invoke(this, EventArgs.Empty);
if (Source is null || !EnableLazyLoading || isInViewport)
{
lazyLoadingSource = null;
@@ -218,23 +97,73 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
AttachImageFailed(OnImageFailed);
base.OnApplyTemplate();
void AttachImageOpened(RoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageOpened += handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageOpened += handler;
}
}
void AttachImageFailed(ExceptionRoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageFailed += handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageFailed += handler;
}
}
void RemoveImageOpened(RoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageOpened -= handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageOpened -= handler;
}
}
void RemoveImageFailed(ExceptionRoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageFailed -= handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageFailed -= handler;
}
}
}
private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageExBase control)
if (d is not ImageExBase control)
{
bool value = (bool)e.NewValue;
if (value)
{
control.LayoutUpdated += control.ImageExBase_LayoutUpdated;
return;
}
control.InvalidateLazyLoading();
}
else
{
control.LayoutUpdated -= control.ImageExBase_LayoutUpdated;
}
bool value = (bool)e.NewValue;
if (value)
{
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
control.InvalidateLazyLoading();
}
else
{
control.LayoutUpdated -= control.OnImageExBaseLayoutUpdated;
}
}
@@ -246,14 +175,6 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
}
}
private static void PlaceholderSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageExBase control)
{
control.OnPlaceholderSourceChanged(e);
}
}
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ImageExBase control)
@@ -261,17 +182,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
return;
}
if (e.OldValue is null || e.NewValue is null || !e.OldValue.Equals(e.NewValue))
if (e.OldValue is not null && e.NewValue is not null && e.OldValue.Equals(e.NewValue))
{
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
{
control.lazyLoadingSource = null;
control.SetSource(e.NewValue);
}
else
{
control.lazyLoadingSource = e.NewValue;
}
return;
}
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
{
control.lazyLoadingSource = null;
control.SetSource(e.NewValue);
}
else
{
control.lazyLoadingSource = e.NewValue;
}
}
@@ -301,11 +224,24 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
{
VisualStateManager.GoToState(this, LoadedState, true);
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
}
}
[SuppressMessage("", "IDE0019")]
private void AttachPlaceholderSource(ImageSource? source)
{
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
// We only need to call those methods if we fail in other cases before we get here.
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
{
image.Source = source;
}
else if (PlaceholderImage is ImageBrush brush)
{
brush.ImageSource = source;
}
}
private async void SetSource(object? source)
{
if (!IsInitialized)
@@ -326,22 +262,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
VisualStateManager.GoToState(this, LoadingState, true);
ImageSource? imageSource = source as ImageSource;
if (imageSource is not null)
if (source as ImageSource is { } imageSource)
{
AttachSource(imageSource);
return;
}
Uri? uri = source as Uri;
if (uri is null)
if (source as Uri is not { } uri)
{
string? url = source as string ?? source.ToString();
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
{
VisualStateManager.GoToState(this, FailedState, true);
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new UriFormatException("Invalid uri specified.")));
return;
}
}
@@ -355,61 +288,131 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
{
await LoadImageAsync(uri, tokenSource.Token).ConfigureAwait(true);
}
catch (Exception ex)
{
SetPlaceholderSource(PlaceholderSource);
if (ex is OperationCanceledException)
{
// nothing to do as cancellation has been requested.
}
else
{
VisualStateManager.GoToState(this, FailedState, true);
}
}
}
private async void SetPlaceholderSource(object? source)
{
if (!IsInitialized)
{
return;
}
tokenSource?.Cancel();
tokenSource = new CancellationTokenSource();
AttachPlaceholderSource(null);
if (source is null)
{
return;
}
if (source as ImageSource is { } imageSource)
{
AttachPlaceholderSource(imageSource);
return;
}
if (source as Uri is not { } uri)
{
string? url = source as string ?? source.ToString();
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
{
return;
}
}
if (!IsHttpUri(uri) && !uri.IsAbsoluteUri)
{
uri = new Uri("ms-appx:///" + uri.OriginalString.TrimStart('/'));
}
try
{
if (uri is null)
{
return;
}
ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
// Only attach our image if we still have a valid request.
AttachPlaceholderSource(img);
}
}
catch (OperationCanceledException)
{
// nothing to do as cancellation has been requested.
}
catch (Exception e)
catch
{
VisualStateManager.GoToState(this, FailedState, true);
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e));
}
}
private async Task LoadImageAsync(Uri imageUri, CancellationToken token)
{
if (imageUri is not null)
if (imageUri is null)
{
if (IsCacheEnabled)
return;
}
if (IsCacheEnabled)
{
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
// Only attach our image if we still have a valid request.
AttachSource(img);
}
}
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
{
string source = imageUri.OriginalString;
const string base64Head = "base64,";
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
if (index >= 0)
{
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
BitmapImage bitmap = new();
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
// Only attach our image if we still have a valid request.
AttachSource(img);
AttachSource(bitmap);
}
}
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
}
else
{
AttachSource(new BitmapImage(imageUri)
{
string source = imageUri.OriginalString;
const string base64Head = "base64,";
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
if (index >= 0)
{
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
BitmapImage bitmap = new();
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
AttachSource(bitmap);
}
}
}
else
{
AttachSource(new BitmapImage(imageUri)
{
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
});
}
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
});
}
}
private void ImageExBase_LayoutUpdated(object? sender, object e)
private void OnImageExBaseLayoutUpdated(object? sender, object e)
{
InvalidateLazyLoading();
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Image.Implementation;
internal sealed class ImageExFailedEventArgs : EventArgs
{
public ImageExFailedEventArgs(Exception errorException)
{
ErrorMessage = ErrorException?.Message;
ErrorException = errorException;
}
public Exception? ErrorException { get; private set; }
public string? ErrorMessage { get; private set; }
}

View File

@@ -1,8 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Image.Implementation;
internal sealed class ImageExOpenedEventArgs : EventArgs
{
}

View File

@@ -9,10 +9,6 @@ using Snap.Hutao.ViewModel.Abstraction;
namespace Snap.Hutao.Control;
/// <summary>
/// 表示支持取消加载的异步页面
/// 在被导航到其他页面前触发取消异步通知
/// </summary>
[HighQuality]
[SuppressMessage("", "CA1001")]
internal class ScopedPage : Page
@@ -21,9 +17,8 @@ internal class ScopedPage : Page
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope currentScope;
/// <summary>
/// 构造一个新的页面
/// </summary>
private bool inFrame = true;
protected ScopedPage()
{
unloadEventHandler = OnUnloaded;
@@ -31,11 +26,6 @@ internal class ScopedPage : Page
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
}
/// <summary>
/// 异步通知接收器
/// </summary>
/// <param name="extra">额外内容</param>
/// <returns>任务</returns>
public async ValueTask NotifyRecipientAsync(INavigationData extra)
{
if (extra.Data is not null && DataContext is INavigationRecipient recipient)
@@ -61,6 +51,32 @@ internal class ScopedPage : Page
/// <inheritdoc/>
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
DisposeViewModel();
inFrame = false;
}
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is INavigationData extra)
{
NotifyRecipientAsync(extra).SafeForget();
}
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
if (inFrame)
{
DisposeViewModel();
}
DataContext = null;
Unloaded -= unloadEventHandler;
}
private void DisposeViewModel()
{
using (viewCancellationTokenSource)
{
@@ -79,19 +95,4 @@ internal class ScopedPage : Page
}
}
}
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is INavigationData extra)
{
NotifyRecipientAsync(extra).SafeForget();
}
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
DataContext = null;
Unloaded -= unloadEventHandler;
}
}

View File

@@ -18,4 +18,10 @@
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="6"/>
</Style>
<Style
x:Key="FlyoutPresenterPadding16And10Style"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="16,10"/>
</Style>
</ResourceDictionary>

View File

@@ -29,6 +29,8 @@
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
<x:String x:Key="UI_EmotionIcon445">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
</ResourceDictionary>

View File

@@ -18,11 +18,11 @@ internal sealed class CommandLineBuilder
/// <summary>
/// 当符合条件时添加参数
/// </summary>
/// <param name="name">参数名称</param>
/// <param name="condition">条件</param>
/// <param name="name">参数名称</param>
/// <param name="value">值</param>
/// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIf(string name, bool condition, object? value = null)
public CommandLineBuilder AppendIf(bool condition, string name, object? value = null)
{
return condition ? Append(name, value) : this;
}
@@ -35,7 +35,7 @@ internal sealed class CommandLineBuilder
/// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIfNotNull(string name, object? value = null)
{
return AppendIf(name, value is not null, value);
return AppendIf(value is not null, name, value);
}
/// <summary>

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
/// <summary>
/// 指示此特性附加的属性会在属性改变后会异步地设置的其他属性
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
internal sealed class AlsoAsyncSetsAttribute : Attribute
{
public AlsoAsyncSetsAttribute(string propertyName)
{
}
public AlsoAsyncSetsAttribute(string propertyName1, string propertyName2)
{
}
public AlsoAsyncSetsAttribute(params string[] propertyNames)
{
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
/// <summary>
/// 指示此特性附加的属性会在属性改变后会设置的其他属性
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
internal sealed class AlsoSetsAttribute : Attribute
{
public AlsoSetsAttribute(string propertyName)
{
}
public AlsoSetsAttribute(string propertyName1, string propertyName2)
{
}
public AlsoSetsAttribute(params string[] propertyNames)
{
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
internal sealed class HutaoException : Exception
{
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
: this(message, innerException)
{
Kind = kind;
}
public HutaoException(string message, Exception? innerException)
: base($"{message}\n{innerException?.Message}", innerException)
{
}
public HutaoExceptionKind Kind { get; private set; }
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
internal enum HutaoExceptionKind
{
None,
}

View File

@@ -49,6 +49,13 @@ internal static class ThrowHelper
throw new NotSupportedException();
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static NotSupportedException NotSupported(string message)
{
throw new NotSupportedException(message);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static OperationCanceledException OperationCanceled(string message, Exception? inner = default)

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO.Hashing;
internal static class SHA256
{
public static async ValueTask<string> HashFileAsync(string filePath, CancellationToken token = default)
{
using (FileStream stream = File.OpenRead(filePath))
{
return await HashAsync(stream, token).ConfigureAwait(false);
}
}
public static async ValueTask<string> HashAsync(Stream stream, CancellationToken token = default)
{
byte[] bytes = await System.Security.Cryptography.SHA256.HashDataAsync(stream, token).ConfigureAwait(false);
return System.Convert.ToHexString(bytes);
}
}

View File

@@ -130,7 +130,7 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
BytesRead = bytesRead;
}
public int BytesRead { get; set; }
public int BytesRead { get; }
}
private sealed class ShardProgress : IProgress<ShardStatus>
@@ -152,11 +152,11 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
public void Report(ShardStatus value)
{
Interlocked.Add(ref totalBytesRead, value.BytesRead);
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000 || totalBytesRead == contentLength)
{
lock (syncRoot)
{
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000 || totalBytesRead == contentLength)
{
workerProgress.Report(statusFactory(totalBytesRead, contentLength));
stopwatch = ValueStopwatch.StartNew();

View File

@@ -65,7 +65,7 @@ internal class StreamCopyWorker<TStatus>
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
totalBytesRead += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
{
progress.Report(statusFactory(totalBytesRead));
stopwatch = ValueStopwatch.StartNew();

View File

@@ -2,6 +2,8 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Setting;
using Windows.Win32.Foundation;
using Windows.Win32.System.Console;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Logging;
@@ -15,7 +17,17 @@ internal sealed class ConsoleWindowLifeTime : IDisposable
if (LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, false))
{
consoleWindowAllocated = AllocConsole();
SetConsoleTitle("Snap Hutao Debug Console");
if (consoleWindowAllocated)
{
HANDLE inputHandle = GetStdHandle(STD_HANDLE.STD_INPUT_HANDLE);
if (GetConsoleMode(inputHandle, out CONSOLE_MODE mode))
{
mode &= ~CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE;
SetConsoleMode(inputHandle, mode);
}
SetConsoleTitle("Snap Hutao Debug Console");
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core;
internal static class RuntimeOptionsExtension
{
public static string GetDataFolderUpdateCacheFolderFile(this RuntimeOptions options, string fileName)
{
string directory = Path.Combine(options.DataFolder, "UpdateCache");
Directory.CreateDirectory(directory);
return Path.Combine(directory, fileName);
}
}

View File

@@ -7,30 +7,30 @@ namespace Snap.Hutao.Core.Setting;
/// 设置键
/// </summary>
[HighQuality]
[SuppressMessage("", "SA1124")]
internal static class SettingKeys
{
#region MainWindow
public const string WindowRect = "WindowRect";
public const string IsNavPaneOpen = "IsNavPaneOpen";
public const string LaunchTimes = "LaunchTimes";
public const string DataFolderPath = "DataFolderPath";
public const string PassportUserName = "PassportUserName";
public const string PassportPassword = "PassportPassword";
public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked";
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds";
#endregion
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
#region Application
public const string LaunchTimes = "LaunchTimes";
public const string DataFolderPath = "DataFolderPath";
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
#endregion
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
#region Passport
public const string PassportUserName = "PassportUserName";
public const string PassportPassword = "PassportPassword";
#endregion
#region Cultivation
public const string CultivationAvatarLevelCurrent = "CultivationAvatarLevelCurrent";
public const string CultivationAvatarLevelTarget = "CultivationAvatarLevelTarget";
public const string CultivationAvatarSkillACurrent = "CultivationAvatarSkillACurrent";
@@ -43,13 +43,18 @@ internal static class SettingKeys
public const string CultivationWeapon90LevelTarget = "CultivationWeapon90LevelTarget";
public const string CultivationWeapon70LevelCurrent = "CultivationWeapon70LevelCurrent";
public const string CultivationWeapon70LevelTarget = "CultivationWeapon70LevelTarget";
#endregion
#region HomeCard Dashboard
public const string IsHomeCardLaunchGamePresented = "IsHomeCardLaunchGamePresented";
public const string IsHomeCardGachaStatisticsPresented = "IsHomeCardGachaStatisticsPresented";
public const string IsHomeCardAchievementPresented = "IsHomeCardAchievementPresented";
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
#endregion
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
#region DevTool
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
#endregion
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Storage;
@@ -19,23 +18,16 @@ namespace Snap.Hutao.Core.Shell;
internal sealed partial class ShellLinkInterop : IShellLinkInterop
{
private readonly RuntimeOptions runtimeOptions;
private readonly AppOptions appOptions;
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
{
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
try
{
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri);
using (Stream inputStream = (await iconFile.OpenReadAsync()).AsStream())
{
using (FileStream outputStream = File.Create(targetLogoPath))
{
await inputStream.CopyToAsync(outputStream).ConfigureAwait(false);
}
}
await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false);
}
catch
{
@@ -45,12 +37,15 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink);
Marshal.ThrowExceptionForHR(result);
shellLink.SetPath(appOptions.PowerShellPath);
shellLink.SetArguments($"""
-Command "Start-Process shell:AppsFolder\{runtimeOptions.FamilyName}!App -verb runas"
""");
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_SHOWMINNOACTIVE);
shellLink.SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
shellLink.SetIconLocation(targetLogoPath, 0);
IShellLinkDataList shellLinkDataList = (IShellLinkDataList)shellLink;
shellLinkDataList.GetFlags(out uint flags);
flags |= (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER;
shellLinkDataList.SetFlags(flags);
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");

View File

@@ -1,45 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Dispatching;
namespace Snap.Hutao.Core.Threading;
internal class DispatcherQueueProgress<T> : IProgress<T>
{
private readonly SynchronizationContext synchronizationContext;
private readonly Action<T>? handler;
private readonly SendOrPostCallback invokeHandlers;
private readonly DispatcherQueue dispatcherQueue;
private readonly Action<T> handler;
public DispatcherQueueProgress(Action<T> handler, SynchronizationContext synchronizationContext)
public DispatcherQueueProgress(Action<T> handler, DispatcherQueue dispatcherQueue)
{
this.synchronizationContext = synchronizationContext;
invokeHandlers = new SendOrPostCallback(InvokeHandlers);
ArgumentNullException.ThrowIfNull(handler);
this.dispatcherQueue = dispatcherQueue;
this.handler = handler;
}
public event EventHandler<T>? ProgressChanged;
public void Report(T value)
{
Action<T>? handler = this.handler;
EventHandler<T>? changedEvent = ProgressChanged;
if (handler is not null || changedEvent is not null)
{
synchronizationContext.Post(invokeHandlers, value);
}
}
[SuppressMessage("", "SH007")]
private void InvokeHandlers(object? state)
{
T value = (T)state!;
Action<T>? handler = this.handler;
EventHandler<T>? changedEvent = ProgressChanged;
handler?.Invoke(value);
changedEvent?.Invoke(this, value);
Action<T> handler = this.handler;
dispatcherQueue.TryEnqueue(() => handler(value));
}
}

View File

@@ -8,8 +8,6 @@ namespace Snap.Hutao.Core.Threading;
/// </summary>
internal interface ITaskContext
{
SynchronizationContext SynchronizationContext { get; }
void BeginInvokeOnMainThread(Action action);
void InvokeOnMainThread(Action action);

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Dispatching;
namespace Snap.Hutao.Core.Threading;
internal interface ITaskContextUnsafe
{
DispatcherQueue DispatcherQueue { get; }
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Threading;
/// 任务上下文
/// </summary>
[Injection(InjectAs.Singleton, typeof(ITaskContext))]
internal sealed class TaskContext : ITaskContext
internal sealed class TaskContext : ITaskContext, ITaskContextUnsafe
{
private readonly DispatcherQueueSynchronizationContext synchronizationContext;
private readonly DispatcherQueue dispatcherQueue;
@@ -24,7 +24,7 @@ internal sealed class TaskContext : ITaskContext
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
}
public SynchronizationContext SynchronizationContext { get => synchronizationContext; }
public DispatcherQueue DispatcherQueue { get => dispatcherQueue; }
/// <inheritdoc/>
public ThreadPoolSwitchOperation SwitchToBackgroundAsync()

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Threading;
internal sealed class Throttler
{
private readonly ConcurrentDictionary<string, SemaphoreSlim> methodSemaphoreMap = new();
public ValueTask<SemaphoreSlimToken> ThrottleAsync(CancellationToken token = default, [CallerMemberName] string callerName = default!, [CallerLineNumber] int callerLine = 0)
{
string key = $"{callerName}L{callerLine}";
SemaphoreSlim semaphore = methodSemaphoreMap.GetOrAdd(key, name => new SemaphoreSlim(1));
return semaphore.EnterAsync(token);
}
}

View File

@@ -2,12 +2,20 @@
// Licensed under the MIT license.
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core;
internal static class UnsafeDateTimeOffset
{
[SuppressMessage("", "SH002")]
public static DateTimeOffset ParseDateTime(ReadOnlySpan<char> span, TimeSpan offset)
{
DateTime dateTime = DateTime.Parse(span, CultureInfo.InvariantCulture);
return new(dateTime, offset);
}
[Pure]
[SuppressMessage("", "SH002")]
public static unsafe DateTimeOffset AdjustOffsetOnly(DateTimeOffset dateTimeOffset, in TimeSpan offset)

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using Windows.Storage;
namespace Snap.Hutao.Extension;
internal static class StorageFileExtension
{
public static async ValueTask OverwriteCopyAsync(this StorageFile file, string targetFile)
{
using (Stream outputStream = (await file.OpenReadAsync()).AsStreamForRead())
{
using (FileStream inputStream = File.Create(targetFile))
{
await outputStream.CopyToAsync(inputStream).ConfigureAwait(false);
}
}
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Factory.Progress;
[ConstructorGenerated]
@@ -11,6 +13,11 @@ internal sealed partial class ProgressFactory : IProgressFactory
public IProgress<T> CreateForMainThread<T>(Action<T> handler)
{
return new DispatcherQueueProgress<T>(handler, taskContext.SynchronizationContext);
if (taskContext is not ITaskContextUnsafe @unsafe)
{
throw ThrowHelper.NotSupported();
}
return new DispatcherQueueProgress<T>(handler, @unsafe.DispatcherQueue);
}
}

View File

@@ -34,7 +34,7 @@
<ListView
Grid.Row="1"
ItemsSource="{Binding GameAccounts}"
ItemsSource="{Binding GameAccountsView}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>

View File

@@ -3,7 +3,6 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Windows.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao;
@@ -20,9 +19,6 @@ internal sealed partial class MainWindow : Window, IWindowOptionsSource, IMinMax
private const int MinHeight = 600;
private readonly WindowOptions windowOptions;
private readonly ILogger<MainWindow> logger;
private readonly TypedEventHandler<object, WindowEventArgs> closedEventHander;
private readonly TypedEventHandler<object, WindowSizeChangedEventArgs> sizeChangedEventHandler;
/// <summary>
/// 构造一个新的主窗体
@@ -33,13 +29,6 @@ internal sealed partial class MainWindow : Window, IWindowOptionsSource, IMinMax
InitializeComponent();
windowOptions = new(this, TitleBarView.DragArea, new(1200, 741), true);
this.InitializeController(serviceProvider);
logger = serviceProvider.GetRequiredService<ILogger<MainWindow>>();
closedEventHander = OnClosed;
sizeChangedEventHandler = OnSizeChanged;
Closed += closedEventHander;
SizeChanged += sizeChangedEventHandler;
}
/// <inheritdoc/>
@@ -51,13 +40,4 @@ internal sealed partial class MainWindow : Window, IWindowOptionsSource, IMinMax
pInfo.ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo.ptMinTrackSize.X);
pInfo.ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo.ptMinTrackSize.Y);
}
private void OnClosed(object sender, WindowEventArgs args)
{
logger.LogInformation("MainWindow Closed");
}
private void OnSizeChanged(object sender, WindowSizeChangedEventArgs args)
{
}
}

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
[HighQuality]
[Table("game_accounts")]
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string>
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string, SchemeType>
{
/// <summary>
/// 内部Id
@@ -40,21 +40,17 @@ internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount,
/// <summary>
/// [MIHOYOSDK_ADL_PROD_CN_h3123967166]
/// [MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810]
/// </summary>
public string MihoyoSDK { get; set; } = default!;
/// <summary>
/// 构造一个新的游戏内账号
/// </summary>
/// <param name="name">名称</param>
/// <param name="sdk">sdk</param>
/// <returns>游戏内账号</returns>
public static GameAccount From(string name, string sdk)
public static GameAccount From(string name, string sdk, SchemeType type)
{
return new()
{
Name = name,
MihoyoSDK = sdk,
Type = type,
};
}

View File

@@ -9,18 +9,18 @@ namespace Snap.Hutao.Model.Entity.Primitive;
[HighQuality]
internal enum SchemeType
{
/// <summary>
/// 国际服
/// </summary>
Hoyoverse,
/// <summary>
/// 国服官服
/// </summary>
Official,
ChineseOfficial,
/// <summary>
/// 国际服
/// </summary>
Oversea,
/// <summary>
/// 渠道服
/// </summary>
Bilibili,
ChineseBilibili,
}

View File

@@ -15,9 +15,7 @@ internal sealed partial class SettingEntry
public const string GamePathEntries = "GamePathEntries";
/// <summary>
/// PowerShell 路径
/// </summary>
[Obsolete("不再使用 PowerShell")]
public const string PowerShellPath = "PowerShellPath";
/// <summary>
@@ -127,4 +125,6 @@ internal sealed partial class SettingEntry
/// 自定义极验接口
/// </summary>
public const string GeetestCustomCompositeUrl = "GeetestCustomCompositeUrl";
public const string AnnouncementRegion = "AnnouncementRegion";
}

View File

@@ -93,6 +93,10 @@ internal static class AvatarIds
public static readonly AvatarId Neuvillette = 10000087;
public static readonly AvatarId Charlotte = 10000088;
public static readonly AvatarId Furina = 10000089;
public static readonly AvatarId Chevreuse = 10000090;
public static readonly AvatarId Navia = 10000091;
public static readonly AvatarId Gaming = 10000092;
public static readonly AvatarId Xianyun = 10000093;
/// <summary>
/// 检查该角色是否为主角

View File

@@ -4,20 +4,22 @@
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"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
IgnorableNamespaces="com uap desktop rescap mp">
IgnorableNamespaces="com uap desktop desktop6 rescap mp">
<Identity
Name="60568DGPStudio.SnapHutao"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.8.5.0" />
Version="1.9.1.0" />
<Properties>
<DisplayName>Snap Hutao</DisplayName>
<PublisherDisplayName>DGP Studio</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
</Properties>
<Dependencies>
@@ -64,5 +66,6 @@
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
<rescap:Capability Name="unvirtualizedResources"/>
</Capabilities>
</Package>

View File

@@ -4,20 +4,22 @@
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"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
IgnorableNamespaces="com uap desktop rescap mp">
IgnorableNamespaces="com uap desktop desktop6 rescap mp">
<Identity
Name="60568DGPStudio.SnapHutaoDev"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.8.4.0" />
Version="1.9.1.0" />
<Properties>
<DisplayName>Snap Hutao Dev</DisplayName>
<PublisherDisplayName>DGP Studio</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
</Properties>
<Dependencies>
@@ -64,5 +66,6 @@
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
<rescap:Capability Name="unvirtualizedResources"/>
</Capabilities>
</Package>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,9 +186,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>Import</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>Select PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>Welcome to Snap Hutao, Traveler ~</value>
</data>
@@ -932,9 +929,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>Unable to set registry key without enabling long path</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShell installation directory not found</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>Unable to read game config file {0}, file may be not exist</value>
</data>
@@ -995,6 +989,9 @@
<data name="ServiceUIGFImportUnsupportedVersion" xml:space="preserve">
<value>Unsupported UIGF version</value>
</data>
<data name="ServiceUpdateStatusVersionDescription" xml:space="preserve">
<value>A new version {0} is available.</value>
</data>
<data name="ServiceUserCurrentMultiMatched" xml:space="preserve">
<value>Multiple users recorded as selected</value>
</data>
@@ -1649,6 +1646,9 @@
<data name="ViewPageAnnouncementGame" xml:space="preserve">
<value>Game Notice</value>
</data>
<data name="ViewPageAnnouncementViewDetails" xml:space="preserve">
<value>View Details</value>
</data>
<data name="ViewPageAvatarPropertyArtifactScore" xml:space="preserve">
<value>Artifacts Rating</value>
</data>
@@ -2306,6 +2306,12 @@
<data name="ViewPageSettingGeetestVerificationHeader" xml:space="preserve">
<value>CAPTCHA</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionDescription" xml:space="preserve">
<value>Select the game server for which you want to get announcements</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionHeader" xml:space="preserve">
<value>Announcement Server</value>
</data>
<data name="ViewpageSettingHomeCardDescription" xml:space="preserve">
<value>Manage cards on home dashboard</value>
</data>
@@ -2420,12 +2426,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>When setting the game path, please select the game program (Yuanshen.exe or GenshinImpact.exe) instead of the game launcher (launcher.exe)</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>Snap Hutao uses PowerShell to modify information in registry to change game accounts</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell Path</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell Experience</value>
</data>
@@ -2612,6 +2612,12 @@
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
<value>Upload Data</value>
</data>
<data name="ViewTitileUpdatePackageReadyContent" xml:space="preserve">
<value>Install now?</value>
</data>
<data name="ViewTitileUpdatePackageReadyTitle" xml:space="preserve">
<value>Snap Hutao version {0} is ready</value>
</data>
<data name="ViewTitleAutoClicking" xml:space="preserve">
<value>Auto Click</value>
</data>
@@ -2706,7 +2712,7 @@
<value>〓Update Maintenance Duration.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTitle" xml:space="preserve">
<value>Version \d\.\d Update Maintenance Preview</value>
<value>Version \d\.\d Update Details</value>
</data>
<data name="WebAnnouncementTimeDaysBeginFormat" xml:space="preserve">
<value>Start in {0} days</value>
@@ -2852,9 +2858,30 @@
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>Copy Link Successful</value>
</data>
<data name="WebHoyolabInvalidRegion" xml:space="preserve">
<value>Invalid server</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>Invalid UID</value>
</data>
<data name="WebHoyolabRegionCNGF01" xml:space="preserve">
<value>CN Server: Official</value>
</data>
<data name="WebHoyolabRegionCNQD01" xml:space="preserve">
<value>CN Server: bilibili</value>
</data>
<data name="WebHoyolabRegionOSASIA" xml:space="preserve">
<value>Oversea Server: Asian</value>
</data>
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
<value>Oversea Server: TW/HK/MU server</value>
</data>
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
<value>Oversea Server: EU</value>
</data>
<data name="WebHoyolabRegionOSUSA" xml:space="preserve">
<value>Oversea Server: NA</value>
</data>
<data name="WebHutaoServiceUnAvailable" xml:space="preserve">
<value>Snap Hutao server is under maintenance</value>
</data>

File diff suppressed because it is too large Load Diff

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,9 +186,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>インポート</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>PowerShellを選択</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>胡桃へようこそ</value>
</data>
@@ -932,9 +929,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>長いパスのサポートがオフになっているため、レジストリキーを編集できません。</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShellのインストールディレクトリが見つかりません</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>ゲーム設定ファイル {0} の読み込みに失敗しました。ファイルが存在していない可能性があります。</value>
</data>
@@ -995,6 +989,9 @@
<data name="ServiceUIGFImportUnsupportedVersion" xml:space="preserve">
<value>サポートされていないUIGFバージョン</value>
</data>
<data name="ServiceUpdateStatusVersionDescription" xml:space="preserve">
<value>发现新版本 {0}</value>
</data>
<data name="ServiceUserCurrentMultiMatched" xml:space="preserve">
<value>ユーザー情報を複数選択しています。</value>
</data>
@@ -1649,6 +1646,9 @@
<data name="ViewPageAnnouncementGame" xml:space="preserve">
<value>重要</value>
</data>
<data name="ViewPageAnnouncementViewDetails" xml:space="preserve">
<value>查看详情</value>
</data>
<data name="ViewPageAvatarPropertyArtifactScore" xml:space="preserve">
<value>聖遺物スコア</value>
</data>
@@ -2306,6 +2306,12 @@
<data name="ViewPageSettingGeetestVerificationHeader" xml:space="preserve">
<value>CAPTCHA</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionDescription" xml:space="preserve">
<value>选择想要获取公告的游戏服务器</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionHeader" xml:space="preserve">
<value>公告所属服务器</value>
</data>
<data name="ViewpageSettingHomeCardDescription" xml:space="preserve">
<value>ダッシュボードを整理する</value>
</data>
@@ -2420,12 +2426,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>ゲームのパスを設定する際、本体YuanShen.exe または GenshinImpact.exeを選んでください。ランチャーlauncher.exeではありません</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃のゲームランチャーはPowershellを介してレジストリを変更し、ゲームで使用するアカウントを変更します。</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell パス</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell エクスペリエンス</value>
</data>
@@ -2612,6 +2612,12 @@
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
<value>データをアップロード</value>
</data>
<data name="ViewTitileUpdatePackageReadyContent" xml:space="preserve">
<value>是否立即安装?</value>
</data>
<data name="ViewTitileUpdatePackageReadyTitle" xml:space="preserve">
<value>胡桃 {0} 版本已准备就绪</value>
</data>
<data name="ViewTitleAutoClicking" xml:space="preserve">
<value>オートクリック</value>
</data>
@@ -2852,9 +2858,30 @@
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>ダウンロードリンクのコピーに成功しました</value>
</data>
<data name="WebHoyolabInvalidRegion" xml:space="preserve">
<value>无效的服务器</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>無効なUIDです</value>
</data>
<data name="WebHoyolabRegionCNGF01" xml:space="preserve">
<value>国服 官方服</value>
</data>
<data name="WebHoyolabRegionCNQD01" xml:space="preserve">
<value>国服 渠道服</value>
</data>
<data name="WebHoyolabRegionOSASIA" xml:space="preserve">
<value>国际服 亚服</value>
</data>
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
<value>国际服 港澳台服</value>
</data>
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
<value>国际服 欧服</value>
</data>
<data name="WebHoyolabRegionOSUSA" xml:space="preserve">
<value>国际服 美服</value>
</data>
<data name="WebHutaoServiceUnAvailable" xml:space="preserve">
<value>胡桃サーバがメンテナンス中です</value>
</data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,9 +186,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>가져오기</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>选择 PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>欢迎使用胡桃</value>
</data>
@@ -932,9 +929,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>긴 경로 기능이 켜지지 않아 레지스트리 키 값을 설정할 수 없습니다</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShell 설치 경로를 찾을 수 없습니다</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>无法读取游戏配置文件 {0},可能是文件不存在</value>
</data>
@@ -995,6 +989,9 @@
<data name="ServiceUIGFImportUnsupportedVersion" xml:space="preserve">
<value>不支持的 UIGF 版本</value>
</data>
<data name="ServiceUpdateStatusVersionDescription" xml:space="preserve">
<value>发现新版本 {0}</value>
</data>
<data name="ServiceUserCurrentMultiMatched" xml:space="preserve">
<value>여러 사용자가 선택되었습니다</value>
</data>
@@ -1649,6 +1646,9 @@
<data name="ViewPageAnnouncementGame" xml:space="preserve">
<value>게임 공지</value>
</data>
<data name="ViewPageAnnouncementViewDetails" xml:space="preserve">
<value>查看详情</value>
</data>
<data name="ViewPageAvatarPropertyArtifactScore" xml:space="preserve">
<value>성유물 점수</value>
</data>
@@ -2306,6 +2306,12 @@
<data name="ViewPageSettingGeetestVerificationHeader" xml:space="preserve">
<value>无感验证</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionDescription" xml:space="preserve">
<value>选择想要获取公告的游戏服务器</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionHeader" xml:space="preserve">
<value>公告所属服务器</value>
</data>
<data name="ViewpageSettingHomeCardDescription" xml:space="preserve">
<value>管理主页仪表板中的卡片</value>
</data>
@@ -2420,12 +2426,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>게임 경로를 설정할 때 런쳐(launcher.exe) 대신 게임(YuanShen.exe 또는 GenshinImpact.exe)를 선택하세요</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃使用 PowerShell 更改注册表中的信息以修改游戏内账号</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell 路径</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell 体验</value>
</data>
@@ -2612,6 +2612,12 @@
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
<value>데이터 업로드</value>
</data>
<data name="ViewTitileUpdatePackageReadyContent" xml:space="preserve">
<value>是否立即安装?</value>
</data>
<data name="ViewTitileUpdatePackageReadyTitle" xml:space="preserve">
<value>胡桃 {0} 版本已准备就绪</value>
</data>
<data name="ViewTitleAutoClicking" xml:space="preserve">
<value>自动连点</value>
</data>
@@ -2852,9 +2858,30 @@
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>下载链接复制成功</value>
</data>
<data name="WebHoyolabInvalidRegion" xml:space="preserve">
<value>无效的服务器</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>无效的 UID</value>
</data>
<data name="WebHoyolabRegionCNGF01" xml:space="preserve">
<value>国服 官方服</value>
</data>
<data name="WebHoyolabRegionCNQD01" xml:space="preserve">
<value>国服 渠道服</value>
</data>
<data name="WebHoyolabRegionOSASIA" xml:space="preserve">
<value>国际服 亚服</value>
</data>
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
<value>国际服 港澳台服</value>
</data>
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
<value>国际服 欧服</value>
</data>
<data name="WebHoyolabRegionOSUSA" xml:space="preserve">
<value>国际服 美服</value>
</data>
<data name="WebHutaoServiceUnAvailable" xml:space="preserve">
<value>胡桃服务维护中</value>
</data>

View File

@@ -120,6 +120,12 @@
<data name="AppDevNameAndVersion" xml:space="preserve">
<value>胡桃 Dev {0}</value>
</data>
<data name="AppElevatedDevNameAndVersion" xml:space="preserve">
<value>胡桃Dev {0} [管理员]</value>
</data>
<data name="AppElevatedNameAndVersion" xml:space="preserve">
<value>胡桃 {0} [管理员]</value>
</data>
<data name="AppName" xml:space="preserve">
<value>胡桃</value>
</data>
@@ -186,9 +192,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>导入</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>选择 PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>欢迎使用胡桃</value>
</data>
@@ -932,9 +935,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>未开启长路径功能,无法设置注册表键值</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>找不到 PowerShell 的安装目录</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>无法读取游戏配置文件 {0},可能是文件不存在</value>
</data>
@@ -995,6 +995,9 @@
<data name="ServiceUIGFImportUnsupportedVersion" xml:space="preserve">
<value>不支持的 UIGF 版本</value>
</data>
<data name="ServiceUpdateStatusVersionDescription" xml:space="preserve">
<value>发现新版本 {0}</value>
</data>
<data name="ServiceUserCurrentMultiMatched" xml:space="preserve">
<value>多个用户记录为选中状态</value>
</data>
@@ -1550,6 +1553,9 @@
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
<value>切换账号失败</value>
</data>
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
<value>无法选择UID [{0}] 对应的账号 [{1}],该账号不属于当前服务器</value>
</data>
<data name="ViewModelSettingActionComplete" xml:space="preserve">
<value>操作完成</value>
</data>
@@ -1649,6 +1655,9 @@
<data name="ViewPageAnnouncementGame" xml:space="preserve">
<value>游戏公告</value>
</data>
<data name="ViewPageAnnouncementViewDetails" xml:space="preserve">
<value>查看详情</value>
</data>
<data name="ViewPageAvatarPropertyArtifactScore" xml:space="preserve">
<value>圣遗物评分</value>
</data>
@@ -2138,6 +2147,9 @@
<data name="ViewPageLaunchGameResourcePreDownloadHeader" xml:space="preserve">
<value>预下载</value>
</data>
<data name="ViewPageLaunchGameSelectGamePath" xml:space="preserve">
<value>选择游戏路径</value>
</data>
<data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve">
<value>该账号尚未绑定实时便笺通知 UID</value>
</data>
@@ -2226,7 +2238,7 @@
<value>创建</value>
</data>
<data name="ViewPageSettingCreateDesktopShortcutDescription" xml:space="preserve">
<value>在桌面上创建默认以管理员方式启动的快捷方式</value>
<value>在桌面上创建默认以管理员身份启动的快捷方式</value>
</data>
<data name="ViewPageSettingCreateDesktopShortcutHeader" xml:space="preserve">
<value>创建快捷方式</value>
@@ -2270,6 +2282,15 @@
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
<value>设备 IP</value>
</data>
<data name="ViewPageSettingElevatedModeDescription" xml:space="preserve">
<value>管理员模式会影响部分功能的可用性与行为</value>
</data>
<data name="ViewPageSettingElevatedModeHeader" xml:space="preserve">
<value>管理员模式</value>
</data>
<data name="ViewPageSettingElevatedModeRestartAction" xml:space="preserve">
<value>以管理员身份重启</value>
</data>
<data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve">
<value>在祈愿记录页面显示或隐藏无记录的历史祈愿活动</value>
</data>
@@ -2306,6 +2327,12 @@
<data name="ViewPageSettingGeetestVerificationHeader" xml:space="preserve">
<value>无感验证</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionDescription" xml:space="preserve">
<value>选择想要获取公告的游戏服务器</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionHeader" xml:space="preserve">
<value>公告所属服务器</value>
</data>
<data name="ViewpageSettingHomeCardDescription" xml:space="preserve">
<value>管理主页仪表板中的卡片</value>
</data>
@@ -2420,12 +2447,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>设置游戏路径时请选择游戏本体YuanShen.exe 或 GenshinImpact.exe而不是启动器launcher.exe</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃使用 PowerShell 更改注册表中的信息以修改游戏内账号</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell 路径</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell 体验</value>
</data>
@@ -2612,6 +2633,12 @@
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
<value>上传数据</value>
</data>
<data name="ViewTitileUpdatePackageReadyContent" xml:space="preserve">
<value>是否立即安装?</value>
</data>
<data name="ViewTitileUpdatePackageReadyTitle" xml:space="preserve">
<value>胡桃 {0} 版本已准备就绪</value>
</data>
<data name="ViewTitleAutoClicking" xml:space="preserve">
<value>自动连点</value>
</data>
@@ -2723,6 +2750,15 @@
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
<value>已复制到剪贴板</value>
</data>
<data name="WebDailyNoteArchonQuestStatusFinished" xml:space="preserve">
<value>全部完成</value>
</data>
<data name="WebDailyNoteArchonQuestStatusNotOpen" xml:space="preserve">
<value>尚未开启</value>
</data>
<data name="WebDailyNoteArchonQuestStatusOngoing" xml:space="preserve">
<value>进行中</value>
</data>
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
<value>已完成</value>
</data>
@@ -2852,9 +2888,30 @@
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>下载链接复制成功</value>
</data>
<data name="WebHoyolabInvalidRegion" xml:space="preserve">
<value>无效的服务器</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>无效的 UID</value>
</data>
<data name="WebHoyolabRegionCNGF01" xml:space="preserve">
<value>国服 官方服</value>
</data>
<data name="WebHoyolabRegionCNQD01" xml:space="preserve">
<value>国服 渠道服</value>
</data>
<data name="WebHoyolabRegionOSASIA" xml:space="preserve">
<value>国际服 亚服</value>
</data>
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
<value>国际服 港澳台服</value>
</data>
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
<value>国际服 欧服</value>
</data>
<data name="WebHoyolabRegionOSUSA" xml:space="preserve">
<value>国际服 美服</value>
</data>
<data name="WebHutaoServiceUnAvailable" xml:space="preserve">
<value>胡桃服务维护中</value>
</data>

File diff suppressed because it is too large Load Diff

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,9 +186,6 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>匯入</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>選擇 PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>歡迎使用胡桃</value>
</data>
@@ -932,9 +929,6 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>未開啓長路徑功能,無法設定注冊表鍵值</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>找不到 PowerShell 的安裝目錄</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>無法讀取遊戲設定檔 {0},可能是檔案不存在</value>
</data>
@@ -995,6 +989,9 @@
<data name="ServiceUIGFImportUnsupportedVersion" xml:space="preserve">
<value>不支援的 UIGF 版本</value>
</data>
<data name="ServiceUpdateStatusVersionDescription" xml:space="preserve">
<value>发现新版本 {0}</value>
</data>
<data name="ServiceUserCurrentMultiMatched" xml:space="preserve">
<value>已选中多条用户记录</value>
</data>
@@ -1649,6 +1646,9 @@
<data name="ViewPageAnnouncementGame" xml:space="preserve">
<value>遊戲公告</value>
</data>
<data name="ViewPageAnnouncementViewDetails" xml:space="preserve">
<value>查看详情</value>
</data>
<data name="ViewPageAvatarPropertyArtifactScore" xml:space="preserve">
<value>聖遺物評分</value>
</data>
@@ -2306,6 +2306,12 @@
<data name="ViewPageSettingGeetestVerificationHeader" xml:space="preserve">
<value>無感驗證</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionDescription" xml:space="preserve">
<value>选择想要获取公告的游戏服务器</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionHeader" xml:space="preserve">
<value>公告所属服务器</value>
</data>
<data name="ViewpageSettingHomeCardDescription" xml:space="preserve">
<value>管理主頁儀表板中的卡片</value>
</data>
@@ -2420,12 +2426,6 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>設置游戲路徑時請選擇游戲本體YuanShen.exe 或 GenshinImpact.exe 而不是啓動器launcher.exe</value>
</data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃使用 PowerShell 更改註冊表中的信息以修改遊戲內賬號</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell 路徑</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell 體驗</value>
</data>
@@ -2612,6 +2612,12 @@
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
<value>上傳資料</value>
</data>
<data name="ViewTitileUpdatePackageReadyContent" xml:space="preserve">
<value>是否立即安装?</value>
</data>
<data name="ViewTitileUpdatePackageReadyTitle" xml:space="preserve">
<value>胡桃 {0} 版本已准备就绪</value>
</data>
<data name="ViewTitleAutoClicking" xml:space="preserve">
<value>自動連續點按</value>
</data>
@@ -2852,9 +2858,30 @@
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>下載連結複製成功</value>
</data>
<data name="WebHoyolabInvalidRegion" xml:space="preserve">
<value>无效的服务器</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>無效的 UID</value>
</data>
<data name="WebHoyolabRegionCNGF01" xml:space="preserve">
<value>国服 官方服</value>
</data>
<data name="WebHoyolabRegionCNQD01" xml:space="preserve">
<value>国服 渠道服</value>
</data>
<data name="WebHoyolabRegionOSASIA" xml:space="preserve">
<value>国际服 亚服</value>
</data>
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
<value>国际服 港澳台服</value>
</data>
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
<value>国际服 欧服</value>
</data>
<data name="WebHoyolabRegionOSUSA" xml:space="preserve">
<value>国际服 美服</value>
</data>
<data name="WebHutaoServiceUnAvailable" xml:space="preserve">
<value>胡桃服務維護中</value>
</data>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
namespace Snap.Hutao.Service.Abstraction;
@@ -14,7 +15,9 @@ internal interface IAnnouncementService
/// <summary>
/// 异步获取游戏公告与活动,通常会进行缓存
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <param name="region">服务器</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>公告包装器</returns>
ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(CancellationToken cancellationToken = default);
ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken cancellationToken = default);
}

View File

@@ -2,10 +2,11 @@
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using Snap.Hutao.Web.Response;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
@@ -25,17 +26,17 @@ internal sealed partial class AnnouncementService : IAnnouncementService
private readonly IMemoryCache memoryCache;
/// <inheritdoc/>
public async ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(CancellationToken cancellationToken = default)
public async ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken cancellationToken = default)
{
// 缓存中存在记录,直接返回
if (memoryCache.TryGetRequiredValue(CacheKey, out AnnouncementWrapper? cache))
if (memoryCache.TryGetRequiredValue($"{CacheKey}.{languageCode}.{region}", out AnnouncementWrapper? cache))
{
return cache;
}
await taskContext.SwitchToBackgroundAsync();
Response<AnnouncementWrapper> announcementWrapperResponse = await announcementClient
.GetAnnouncementsAsync(cancellationToken)
.GetAnnouncementsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
if (!announcementWrapperResponse.IsOk())
@@ -45,7 +46,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
AnnouncementWrapper wrapper = announcementWrapperResponse.Data;
Response<ListWrapper<AnnouncementContent>> announcementContentResponse = await announcementClient
.GetAnnouncementContentsAsync(cancellationToken)
.GetAnnouncementContentsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
if (!announcementContentResponse.IsOk())
@@ -61,12 +62,12 @@ internal sealed partial class AnnouncementService : IAnnouncementService
// 将活动公告置于前方
wrapper.List.Reverse();
PreprocessAnnouncements(contentMap, wrapper.List);
PreprocessAnnouncements(contentMap, wrapper.List, new(wrapper.TimeZone, 0, 0));
return memoryCache.Set(CacheKey, wrapper, TimeSpan.FromMinutes(30));
}
private static void PreprocessAnnouncements(Dictionary<int, string> contentMap, List<AnnouncementListWrapper> announcementListWrappers)
private static void PreprocessAnnouncements(Dictionary<int, string> contentMap, List<AnnouncementListWrapper> announcementListWrappers, in TimeSpan offset)
{
// 将公告内容联入公告列表
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
@@ -78,7 +79,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
}
}
AdjustAnnouncementTime(announcementListWrappers);
AdjustAnnouncementTime(announcementListWrappers, offset);
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
{
@@ -90,7 +91,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
}
}
private static void AdjustAnnouncementTime(List<AnnouncementListWrapper> announcementListWrappers)
private static void AdjustAnnouncementTime(List<AnnouncementListWrapper> announcementListWrappers, in TimeSpan offset)
{
// 活动公告
List<Announcement> activities = announcementListWrappers
@@ -103,12 +104,12 @@ internal sealed partial class AnnouncementService : IAnnouncementService
.List
.Single(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title));
if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is not { Success: true } match)
if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is not { Success: true } versionMatch)
{
return;
}
DateTimeOffset versionUpdateTime = DateTimeOffset.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture);
DateTimeOffset versionUpdateTime = UnsafeDateTimeOffset.ParseDateTime(versionMatch.Groups[1].ValueSpan, offset);
foreach (ref readonly Announcement announcement in CollectionsMarshal.AsSpan(activities))
{
@@ -128,7 +129,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
if (AnnouncementRegex.TransientActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } transient)
{
announcement.StartTime = versionUpdateTime;
announcement.EndTime = DateTimeOffset.Parse(transient.Groups[2].ValueSpan, CultureInfo.InvariantCulture);
announcement.EndTime = UnsafeDateTimeOffset.ParseDateTime(transient.Groups[2].ValueSpan, offset);
continue;
}
@@ -138,7 +139,12 @@ internal sealed partial class AnnouncementService : IAnnouncementService
continue;
}
List<DateTimeOffset> dateTimes = matches.Select(match => DateTimeOffset.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture)).ToList();
List<DateTimeOffset> dateTimes = [];
foreach (Match timeMatch in (IList<Match>)matches)
{
dateTimes.Add(UnsafeDateTimeOffset.ParseDateTime(timeMatch.Groups[1].ValueSpan, offset));
}
DateTimeOffset min = DateTimeOffset.MaxValue;
DateTimeOffset max = DateTimeOffset.MinValue;

View File

@@ -1,13 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Primitives;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab;
using System.Globalization;
using System.IO;
namespace Snap.Hutao.Service;
@@ -15,41 +14,12 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions
{
private string? powerShellPath;
private bool? isEmptyHistoryWishVisible;
private BackdropType? backdropType;
private CultureInfo? currentCulture;
private Region? region;
private string? geetestCustomCompositeUrl;
public string PowerShellPath
{
get
{
return GetOption(ref powerShellPath, SettingEntry.PowerShellPath, GetDefaultPowerShellLocationOrEmpty);
static string GetDefaultPowerShellLocationOrEmpty()
{
string? paths = Environment.GetEnvironmentVariable("Path");
if (!string.IsNullOrEmpty(paths))
{
foreach (StringSegment path in new StringTokenizer(paths, [';']))
{
if (path is { HasValue: true, Length: > 0 })
{
if (path.Value.Contains("WindowsPowerShell", StringComparison.OrdinalIgnoreCase))
{
return Path.Combine(path.Value, "powershell.exe");
}
}
}
}
return string.Empty;
}
}
set => SetOption(ref powerShellPath, SettingEntry.PowerShellPath, value);
}
public bool IsEmptyHistoryWishVisible
{
get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible);
@@ -72,6 +42,14 @@ internal sealed partial class AppOptions : DbStoreOptions
set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name);
}
public Lazy<List<NameValue<Region>>> LazyRegions { get; } = new(KnownRegions.Get);
public Region Region
{
get => GetOption(ref region, SettingEntry.AnnouncementRegion, v => Region.FromRegionString(v), Region.CNGF01).Value;
set => SetOption(ref region, SettingEntry.AnnouncementRegion, value, value => value.ToStringOrEmpty());
}
public string GeetestCustomCompositeUrl
{
get => GetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl);

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model;
using Snap.Hutao.Web.Hoyolab;
using System.Globalization;
namespace Snap.Hutao.Service;
@@ -12,4 +13,9 @@ internal static class AppOptionsExtension
{
return appOptions.Cultures.SingleOrDefault(c => c.Value == appOptions.CurrentCulture);
}
public static NameValue<Region>? GetCurrentRegionForSelectionOrDefault(this AppOptions appOptions)
{
return appOptions.LazyRegions.Value.SingleOrDefault(c => c.Value.Value == appOptions.Region.Value);
}
}

View File

@@ -77,7 +77,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
List<DailyNoteEntry> entryList = await dailyNoteDbService.GetDailyNoteEntryIncludeUserListAsync().ConfigureAwait(false);
entryList.ForEach(entry => { entry.UserGameRole = userService.GetUserGameRoleByUid(entry.Uid); });
entries = new(entryList);
entries = entryList.ToObservableCollection();
}
return entries;
@@ -147,7 +147,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
// 发送通知必须早于数据库更新,否则会导致通知重复
await dailyNoteNotificationOperation.SendAsync(entry).ConfigureAwait(false);
await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false);
await dailyNoteWebhookOperation.TryPostDailyNoteToWebhookAsync(dailyNote).ConfigureAwait(false);
await dailyNoteWebhookOperation.TryPostDailyNoteToWebhookAsync(entry.Uid, dailyNote).ConfigureAwait(false);
}
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using System.Net.Http;
@@ -18,7 +19,7 @@ internal sealed partial class DailyNoteWebhookOperation
private readonly DailyNoteOptions dailyNoteOptions;
private readonly HttpClient httpClient;
public async ValueTask TryPostDailyNoteToWebhookAsync(WebDailyNote dailyNote, CancellationToken token = default)
public async ValueTask TryPostDailyNoteToWebhookAsync(PlayerUid playerUid, WebDailyNote dailyNote, CancellationToken token = default)
{
string? targetUrl = dailyNoteOptions.WebhookUrl;
if (string.IsNullOrEmpty(targetUrl) || !Uri.TryCreate(targetUrl, UriKind.Absolute, out Uri? targetUri))
@@ -28,6 +29,7 @@ internal sealed partial class DailyNoteWebhookOperation
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(targetUri)
.SetHeader("x-uid", $"{playerUid}")
.PostJson(dailyNote);
await builder.TryCatchSendAsync(httpClient, logger, token).ConfigureAwait(false);

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.View.Dialog;
using System.Collections.ObjectModel;
@@ -16,7 +17,6 @@ internal sealed partial class GameAccountService : IGameAccountService
private readonly IContentDialogFactory contentDialogFactory;
private readonly IGameDbService gameDbService;
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
private ObservableCollection<GameAccount>? gameAccounts;
@@ -25,77 +25,56 @@ internal sealed partial class GameAccountService : IGameAccountService
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
}
public async ValueTask<GameAccount?> DetectGameAccountAsync()
public async ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType)
{
ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
string? registrySdk = RegistryInterop.Get(schemeType);
if (string.IsNullOrEmpty(registrySdk))
{
GameAccount? account = null;
try
{
account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
if (account is null)
{
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
account = GameAccount.From(name, registrySdk);
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false);
// sync cache
await taskContext.SwitchToMainThreadAsync();
gameAccounts.Add(account);
}
}
return account;
return default;
}
return default;
GameAccount? account = SingleGameAccountOrDefault(gameAccounts, registrySdk);
if (account is null)
{
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
account = GameAccount.From(name, registrySdk, schemeType);
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false);
// sync cache
await taskContext.SwitchToMainThreadAsync();
gameAccounts.Add(account);
}
}
return account;
}
public GameAccount? DetectCurrentGameAccount()
public GameAccount? DetectCurrentGameAccount(SchemeType schemeType)
{
ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get();
string? registrySdk = RegistryInterop.Get(schemeType);
if (!string.IsNullOrEmpty(registrySdk))
if (string.IsNullOrEmpty(registrySdk))
{
try
{
return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
return default;
}
return null;
return SingleGameAccountOrDefault(gameAccounts, registrySdk);
}
public bool SetGameAccount(GameAccount account)
{
if (string.IsNullOrEmpty(appOptions.PowerShellPath))
{
ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!);
}
return RegistryInterop.Set(account, appOptions.PowerShellPath);
return RegistryInterop.Set(account);
}
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
@@ -106,12 +85,12 @@ internal sealed partial class GameAccountService : IGameAccountService
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
{
await taskContext.SwitchToMainThreadAsync();
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
await taskContext.SwitchToMainThreadAsync();
gameAccount.UpdateName(name);
// sync database
@@ -122,11 +101,24 @@ internal sealed partial class GameAccountService : IGameAccountService
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
{
await taskContext.SwitchToMainThreadAsync();
ArgumentNullException.ThrowIfNull(gameAccounts);
await taskContext.SwitchToMainThreadAsync();
gameAccounts.Remove(gameAccount);
await taskContext.SwitchToBackgroundAsync();
await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false);
}
private static GameAccount? SingleGameAccountOrDefault(ObservableCollection<GameAccount> gameAccounts, string registrySdk)
{
try
{
return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
throw ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game.Account;
@@ -12,9 +13,9 @@ internal interface IGameAccountService
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
GameAccount? DetectCurrentGameAccount();
GameAccount? DetectCurrentGameAccount(SchemeType schemeType);
ValueTask<GameAccount?> DetectGameAccountAsync();
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType);
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);

View File

@@ -4,8 +4,7 @@
using Microsoft.Win32;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using System.Diagnostics;
using System.IO;
using Snap.Hutao.Model.Entity.Primitive;
using System.Runtime.InteropServices;
using System.Text;
@@ -16,52 +15,21 @@ namespace Snap.Hutao.Service.Game.Account;
/// </summary>
internal static class RegistryInterop
{
private const string GenshinPath = @"Software\miHoYo\原神";
private const string GenshinKey = $@"HKEY_CURRENT_USER\{GenshinPath}";
private const string SdkChineseKey = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
private const string ChineseKeyName = @"HKEY_CURRENT_USER\Software\miHoYo\原神";
private const string OverseaKeyName = @"HKEY_CURRENT_USER\Software\miHoYo\Genshin Impact";
private const string SdkChineseValueName = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
private const string SdkOverseaValueName = "MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810";
/// <summary>
/// 设置键值
/// 需要支持
/// https://learn.microsoft.com/zh-cn/windows/win32/fileio/maximum-file-path-limitation
/// </summary>
/// <param name="account">账户</param>
/// <param name="powerShellPath">PowerShell 路径</param>
/// <returns>账号是否设置</returns>
public static bool Set(GameAccount? account, string powerShellPath)
public static bool Set(GameAccount? account)
{
if (account is not null)
{
// 存回注册表的字节需要 '\0' 结尾
Encoding.UTF8.GetByteCount(account.MihoyoSDK);
byte[] tempBytes = Encoding.UTF8.GetBytes(account.MihoyoSDK);
byte[] target = new byte[tempBytes.Length + 1];
tempBytes.CopyTo(target, 0);
byte[] target = [.. Encoding.UTF8.GetBytes(account.MihoyoSDK), 0];
(string keyName, string valueName) = GetKeyValueName(account.Type);
Registry.SetValue(keyName, valueName, target);
string base64 = Convert.ToBase64String(target);
string path = $"HKCU:{GenshinPath}";
string command = $"""
-Command "$value = [Convert]::FromBase64String('{base64}'); Set-ItemProperty -Path '{path}' -Name '{SdkChineseKey}' -Value $value -Force;"
""";
ProcessStartInfo startInfo = new()
{
Arguments = command,
WorkingDirectory = Path.GetDirectoryName(powerShellPath),
CreateNoWindow = true,
FileName = powerShellPath,
};
try
{
System.Diagnostics.Process.Start(startInfo)?.WaitForExit();
}
catch (Win32Exception ex)
{
ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropLongPathsDisabled, ex);
}
if (Get() == account.MihoyoSDK)
if (Get(account.Type) == account.MihoyoSDK)
{
return true;
}
@@ -70,24 +38,31 @@ internal static class RegistryInterop
return false;
}
/// <summary>
/// 在注册表中获取账号信息
/// </summary>
/// <returns>当前注册表中的信息</returns>
public static unsafe string? Get()
public static unsafe string? Get(SchemeType scheme)
{
object? sdk = Registry.GetValue(GenshinKey, SdkChineseKey, Array.Empty<byte>());
(string keyName, string valueName) = GetKeyValueName(scheme);
object? sdk = Registry.GetValue(keyName, valueName, Array.Empty<byte>());
if (sdk is byte[] bytes)
if (sdk is not byte[] bytes)
{
fixed (byte* pByte = bytes)
{
// 从注册表获取的字节数组带有 '\0' 结尾,需要舍去
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
return Encoding.UTF8.GetString(span);
}
return null;
}
return null;
fixed (byte* pByte = bytes)
{
// 从注册表获取的字节数组带有 '\0' 结尾,需要舍去
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
return Encoding.UTF8.GetString(span);
}
}
private static (string KeyName, string ValueName) GetKeyValueName(SchemeType scheme)
{
return scheme switch
{
SchemeType.ChineseOfficial => (ChineseKeyName, SdkChineseValueName),
SchemeType.Oversea => (OverseaKeyName, SdkOverseaValueName),
_ => throw ThrowHelper.NotSupported($"Invalid account SchemeType: {scheme}"),
};
}
}

View File

@@ -34,21 +34,6 @@ internal readonly struct ChannelOptions
/// </summary>
public readonly string? ConfigFilePath;
/// <summary>
/// 构造一个新的多通道
/// </summary>
/// <param name="channel">通道</param>
/// <param name="subChannel">子通道</param>
/// <param name="isOversea">是否为国际服</param>
/// <param name="configFilePath">配置文件路径</param>
public ChannelOptions(string? channel, string? subChannel, bool isOversea, string? configFilePath = null)
{
_ = Enum.TryParse(channel, out Channel);
_ = Enum.TryParse(subChannel, out SubChannel);
IsOversea = isOversea;
ConfigFilePath = configFilePath;
}
public ChannelOptions(ChannelType channel, SubChannelType subChannel, bool isOversea)
{
Channel = channel;
@@ -56,24 +41,33 @@ internal readonly struct ChannelOptions
IsOversea = isOversea;
}
/// <summary>
/// 配置文件未找到
/// </summary>
/// <param name="isOversea">是否为国际服</param>
/// <param name="configFilePath">配置文件期望路径</param>
/// <returns>选项</returns>
public ChannelOptions(string? channel, string? subChannel, bool isOversea)
{
_ = Enum.TryParse(channel, out Channel);
_ = Enum.TryParse(subChannel, out SubChannel);
IsOversea = isOversea;
}
private ChannelOptions(bool isOversea, string? configFilePath)
{
IsOversea = isOversea;
ConfigFilePath = configFilePath;
}
public static ChannelOptions FileNotFound(bool isOversea, string configFilePath)
{
return new(null, null, isOversea, configFilePath);
return new(isOversea, configFilePath);
}
/// <inheritdoc/>
public override string ToString()
{
return $"[ChannelType:{Channel}] [SubChannel:{SubChannel}] [IsOversea: {IsOversea}]";
return $$"""
{ ChannelType: {{Channel}}, SubChannel: {{SubChannel}}, IsOversea: {{IsOversea}}}
""";
}
// DO NOT DELETE used in HashSet
// DO NOT DELETE, used in HashSet
public override int GetHashCode()
{
return HashCode.Combine(Channel, SubChannel, IsOversea);

View File

@@ -17,9 +17,12 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
public ChannelOptions GetChannelOptions()
{
string gamePath = launchOptions.GamePath;
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase);
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath))
{
throw ThrowHelper.InvalidOperation($"Invalid game path: {gamePath}");
}
bool isOversea = LaunchScheme.ExecutableIsOversea(Path.GetFileName(gamePath));
if (!File.Exists(configPath))
{
@@ -38,10 +41,10 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
public bool SetChannelOptions(LaunchScheme scheme)
{
string gamePath = launchOptions.GamePath;
string? directory = Path.GetDirectoryName(gamePath);
ArgumentException.ThrowIfNullOrEmpty(directory);
string configPath = Path.Combine(directory, ConfigFileName);
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath))
{
return false;
}
List<IniElement> elements = default!;
try
@@ -70,14 +73,16 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
{
if (element is IniParameter parameter)
{
if (parameter.Key == "channel")
if (parameter.Key is ChannelOptions.ChannelName)
{
changed = parameter.Set(scheme.Channel.ToString("D")) || changed;
continue;
}
if (parameter.Key == "sub_channel")
if (parameter.Key is ChannelOptions.SubChannelName)
{
changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed;
continue;
}
}
}

View File

@@ -9,38 +9,13 @@ namespace Snap.Hutao.Service.Game;
[HighQuality]
internal static class GameConstants
{
/// <summary>
/// 设置文件
/// </summary>
public const string ConfigFileName = "config.ini";
/// <summary>
/// 国服文件名
/// </summary>
public const string YuanShenFileName = "YuanShen.exe";
/// <summary>
/// 外服文件名
/// </summary>
public const string YuanShenFileNameUpper = "YUANSHEN.EXE";
public const string GenshinImpactFileName = "GenshinImpact.exe";
/// <summary>
/// 国服数据文件夹
/// </summary>
public const string GenshinImpactFileNameUpper = "GENSHINIMPACT.EXE";
public const string YuanShenData = "YuanShen_Data";
/// <summary>
/// 国际服数据文件夹
/// </summary>
public const string GenshinImpactData = "GenshinImpact_Data";
/// <summary>
/// 国服进程名
/// </summary>
public const string YuanShenProcessName = "YuanShen";
/// <summary>
/// 外服进程名
/// </summary>
public const string GenshinImpactProcessName = "GenshinImpact";
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
@@ -51,15 +52,15 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
}
/// <inheritdoc/>
public ValueTask<GameAccount?> DetectGameAccountAsync()
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
{
return gameAccountService.DetectGameAccountAsync();
return gameAccountService.DetectGameAccountAsync(scheme);
}
/// <inheritdoc/>
public GameAccount? DetectCurrentGameAccount()
public GameAccount? DetectCurrentGameAccount(SchemeType scheme)
{
return gameAccountService.DetectCurrentGameAccount();
return gameAccountService.DetectCurrentGameAccount(scheme);
}
/// <inheritdoc/>

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Scheme;
namespace Snap.Hutao.Service.Game;
internal static class GameServiceFacadeExtension
{
public static GameAccount? DetectCurrentGameAccount(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme)
{
return gameServiceFacade.DetectCurrentGameAccount(scheme.GetSchemeType());
}
public static ValueTask<GameAccount?> DetectGameAccountAsync(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme)
{
return gameServiceFacade.DetectGameAccountAsync(scheme.GetSchemeType());
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.Scheme;
@@ -28,7 +29,7 @@ internal interface IGameServiceFacade
/// <param name="uid">uid</param>
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
ValueTask<GameAccount?> DetectGameAccountAsync();
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme);
/// <summary>
/// 异步获取游戏路径
@@ -86,9 +87,5 @@ internal interface IGameServiceFacade
/// <returns>是否更改了ini文件</returns>
bool SetChannelOptions(LaunchScheme scheme);
/// <summary>
/// 检测账号
/// </summary>
/// <returns>账号</returns>
GameAccount? DetectCurrentGameAccount();
GameAccount? DetectCurrentGameAccount(SchemeType scheme);
}

View File

@@ -9,12 +9,25 @@ namespace Snap.Hutao.Service.Game;
internal static class LaunchOptionsExtension
{
public static bool TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
public static bool TryGetGamePathAndGameDirectory(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameDirectory)
{
gamePath = options.GamePath;
gameDirectory = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameDirectory))
{
return false;
}
return true;
}
public static bool TryGetGameDirectoryAndGameFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameDirectory, [NotNullWhen(true)] out string? gameFileName)
{
string gamePath = options.GamePath;
gameFolder = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameFolder))
gameDirectory = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameDirectory))
{
gameFileName = default;
return false;
@@ -42,6 +55,18 @@ internal static class LaunchOptionsExtension
return true;
}
public static bool TryGetGamePathAndFilePathByName(this LaunchOptions options, string fileName, out string gamePath, [NotNullWhen(true)] out string? filePath)
{
if (options.TryGetGamePathAndGameDirectory(out gamePath, out string? gameDirectory))
{
filePath = Path.Combine(gameDirectory, fileName);
return true;
}
filePath = default;
return false;
}
public static ImmutableList<GamePathEntry> GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry)
{
string gamePath = options.GamePath;

View File

@@ -4,20 +4,13 @@
namespace Snap.Hutao.Service.Game.Locator;
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocatorFactory))]
[Injection(InjectAs.Singleton, typeof(IGameLocatorFactory))]
internal sealed partial class GameLocatorFactory : IGameLocatorFactory
{
[SuppressMessage("", "SH301")]
private readonly IServiceProvider serviceProvider;
public IGameLocator Create(GameLocationSource source)
{
return source switch
{
GameLocationSource.Registry => serviceProvider.GetRequiredService<RegistryLauncherLocator>(),
GameLocationSource.UnityLog => serviceProvider.GetRequiredService<UnityLogGameLocator>(),
GameLocationSource.Manual => serviceProvider.GetRequiredService<ManualGameLocator>(),
_ => throw Must.NeverHappen(),
};
return serviceProvider.GetRequiredKeyedService<IGameLocator>(source);
}
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Locator;
internal static class GameLocatorFactoryExtensions
{
public static ValueTask<ValueResult<bool, string>> LocateAsync(this IGameLocatorFactory factory, GameLocationSource source)
{
return factory.Create(source).LocateGamePathAsync();
}
}

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient)]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Manual)]
internal sealed partial class ManualGameLocator : IGameLocator
{
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
@@ -26,7 +26,7 @@ internal sealed partial class ManualGameLocator : IGameLocator
if (isPickerOk)
{
string fileName = System.IO.Path.GetFileName(file);
if (fileName is GameConstants.YuanShenFileName or GameConstants.GenshinImpactFileName)
if (fileName.ToUpperInvariant() is GameConstants.YuanShenFileNameUpper or GameConstants.GenshinImpactFileNameUpper)
{
return ValueTask.FromResult<ValueResult<bool, string>>(new(true, file));
}

View File

@@ -13,9 +13,10 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient)]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Registry)]
internal sealed partial class RegistryLauncherLocator : IGameLocator
{
private const string RegistryKeyName = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神";
private readonly ITaskContext taskContext;
/// <inheritdoc/>
@@ -29,50 +30,37 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
{
return result;
}
else
{
string? path = Path.GetDirectoryName(result.Value);
ArgumentException.ThrowIfNullOrEmpty(path);
string configPath = Path.Combine(path, GameConstants.ConfigFileName);
string? escapedPath;
using (FileStream stream = File.OpenRead(configPath))
{
IEnumerable<IniElement> elements = IniSerializer.Deserialize(stream);
escapedPath = elements
.OfType<IniParameter>()
.FirstOrDefault(p => p.Key == "game_install_path")?.Value;
}
if (escapedPath is not null)
{
string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName);
return new(true, gamePath);
}
string? path = Path.GetDirectoryName(result.Value);
ArgumentException.ThrowIfNullOrEmpty(path);
string configPath = Path.Combine(path, GameConstants.ConfigFileName);
string? escapedPath;
using (FileStream stream = File.OpenRead(configPath))
{
IEnumerable<IniElement> elements = IniSerializer.Deserialize(stream);
escapedPath = elements
.OfType<IniParameter>()
.FirstOrDefault(p => p.Key == "game_install_path")?.Value;
}
if (!string.IsNullOrEmpty(escapedPath))
{
string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName);
return new(true, gamePath);
}
return new(false, string.Empty);
}
private static ValueResult<bool, string> LocateInternal(string key)
private static ValueResult<bool, string> LocateInternal(string valueName)
{
using (RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神"))
if (Registry.GetValue(RegistryKeyName, valueName, null) is string path)
{
if (uninstallKey is not null)
{
if (uninstallKey.GetValue(key) is string path)
{
return new(true, path);
}
else
{
return new(false, default!);
}
}
else
{
return new(false, default!);
}
return new(true, path);
}
return new(false, default!);
}
private static string Unescape(string str)

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient)]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.UnityLog)]
internal sealed partial class UnityLogGameLocator : IGameLocator
{
private readonly ITaskContext taskContext;

View File

@@ -21,7 +21,7 @@ internal sealed partial class GamePackageService : IGamePackageService
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
if (!launchOptions.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName))
{
return false;
}
@@ -47,8 +47,7 @@ internal sealed partial class GamePackageService : IGamePackageService
if (!launchScheme.ExecutableMatches(gameFileName))
{
// We can't start the game
// when we failed to convert game
// We can't start the game when we failed to convert game
if (!await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false))
{
return false;
@@ -67,6 +66,13 @@ internal sealed partial class GamePackageService : IGamePackageService
private static bool CheckDirectoryPermissions(string folder)
{
// Program Files has special permissions limitation.
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
if (folder.StartsWith(programFiles, StringComparison.OrdinalIgnoreCase))
{
return false;
}
try
{
string tempFilePath = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp");

View File

@@ -15,6 +15,7 @@ using System.IO.Compression;
using System.Net.Http;
using System.Text.RegularExpressions;
using static Snap.Hutao.Service.Game.GameConstants;
using RelativePathVersionItemDictionary = System.Collections.Generic.Dictionary<string, Snap.Hutao.Service.Game.Package.VersionItem>;
namespace Snap.Hutao.Service.Game.Package;
@@ -58,15 +59,15 @@ internal sealed partial class PackageConverter
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}";
PackageConvertContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
PackageConverterFileSystemContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
// Step 1
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
Dictionary<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
Dictionary<string, VersionItem> localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
RelativePathVersionItemDictionary remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
RelativePathVersionItemDictionary localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
// Step 2
List<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
List<PackageItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
diffOperations.SortBy(i => i.Type);
// Step 3
@@ -116,16 +117,16 @@ internal sealed partial class PackageConverter
}
}
private static IEnumerable<ItemOperationInfo> GetItemOperationInfos(Dictionary<string, VersionItem> remote, Dictionary<string, VersionItem> local)
private static IEnumerable<PackageItemOperationInfo> GetItemOperationInfos(RelativePathVersionItemDictionary remote, RelativePathVersionItemDictionary local)
{
foreach ((string remoteName, VersionItem remoteItem) in remote)
{
if (local.TryGetValue(remoteName, out VersionItem? localItem))
{
if (!remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase))
if (!(remoteItem.FileSize == localItem.FileSize && remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase)))
{
// 本地发现了同名且不同 MD5 的项,需要替换为服务器上的项
yield return new(ItemOperationType.Replace, remoteItem, localItem);
yield return new(PackageItemOperationType.Replace, remoteItem, localItem);
}
// 同名同MD5跳过
@@ -134,22 +135,22 @@ internal sealed partial class PackageConverter
else
{
// 本地没有发现同名项
yield return new(ItemOperationType.Add, remoteItem, remoteItem);
yield return new(PackageItemOperationType.Add, remoteItem, remoteItem);
}
}
foreach ((_, VersionItem localItem) in local)
{
yield return new(ItemOperationType.Backup, localItem, localItem);
yield return new(PackageItemOperationType.Backup, localItem, localItem);
}
}
[GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")]
private static partial Regex DataFolderRegex();
private async ValueTask<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream)
private async ValueTask<RelativePathVersionItemDictionary> GetVersionItemsAsync(Stream stream)
{
Dictionary<string, VersionItem> results = [];
RelativePathVersionItemDictionary results = [];
using (StreamReader reader = new(stream))
{
while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } row)
@@ -164,7 +165,7 @@ internal sealed partial class PackageConverter
return results;
}
private async ValueTask<Dictionary<string, VersionItem>> GetRemoteItemsAsync(string pkgVersionUrl)
private async ValueTask<RelativePathVersionItemDictionary> GetRemoteItemsAsync(string pkgVersionUrl)
{
try
{
@@ -179,7 +180,7 @@ internal sealed partial class PackageConverter
}
}
private async ValueTask<Dictionary<string, VersionItem>> GetLocalItemsAsync(string gameFolder)
private async ValueTask<RelativePathVersionItemDictionary> GetLocalItemsAsync(string gameFolder)
{
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion)))
{
@@ -187,23 +188,23 @@ internal sealed partial class PackageConverter
}
}
private async ValueTask PrepareCacheFilesAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
{
foreach (ItemOperationInfo info in operations)
foreach (PackageItemOperationInfo info in operations)
{
switch (info.Type)
{
case ItemOperationType.Backup:
case PackageItemOperationType.Backup:
continue;
case ItemOperationType.Replace:
case ItemOperationType.Add:
case PackageItemOperationType.Replace:
case PackageItemOperationType.Add:
await SkipOrDownloadAsync(info, context, progress).ConfigureAwait(false);
break;
}
}
}
private async ValueTask SkipOrDownloadAsync(ItemOperationInfo info, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
{
// 还原正确的远程地址
string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName);
@@ -257,16 +258,16 @@ internal sealed partial class PackageConverter
}
}
private async ValueTask<bool> ReplaceGameResourceAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
{
// 执行下载与移动操作
foreach (ItemOperationInfo info in operations)
foreach (PackageItemOperationInfo info in operations)
{
(bool moveToBackup, bool moveToTarget) = info.Type switch
{
ItemOperationType.Backup => (true, false),
ItemOperationType.Replace => (true, true),
ItemOperationType.Add => (false, true),
PackageItemOperationType.Backup => (true, false),
PackageItemOperationType.Replace => (true, true),
PackageItemOperationType.Add => (false, true),
_ => (false, false),
};
@@ -321,7 +322,7 @@ internal sealed partial class PackageConverter
return true;
}
private async ValueTask ReplacePackageVersionFilesAsync(PackageConvertContext context)
private async ValueTask ReplacePackageVersionFilesAsync(PackageConverterFileSystemContext context)
{
foreach (string versionFilePath in Directory.EnumerateFiles(context.GameFolder, "*pkg_version"))
{

View File

@@ -6,7 +6,7 @@ using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Package;
internal readonly struct PackageConvertContext
internal readonly struct PackageConverterFileSystemContext
{
public readonly string GameFolder;
public readonly string ServerCacheFolder;
@@ -22,7 +22,7 @@ internal readonly struct PackageConvertContext
public readonly string ScatteredFilesUrl;
public readonly string PkgVersionUrl;
public PackageConvertContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl)
public PackageConverterFileSystemContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl)
{
GameFolder = gameFolder;
ServerCacheFolder = Path.Combine(dataFolder, "ServerCache");
@@ -37,7 +37,8 @@ internal readonly struct PackageConvertContext
? (YuanShenData, GenshinImpactData)
: (GenshinImpactData, YuanShenData);
(FromDataFolder, ToDataFolder) = (Path.Combine(GameFolder, FromDataFolderName), Path.Combine(GameFolder, ToDataFolderName));
FromDataFolder = Path.Combine(GameFolder, FromDataFolderName);
ToDataFolder = Path.Combine(GameFolder, ToDataFolderName);
ScatteredFilesUrl = scatteredFilesUrl;
PkgVersionUrl = $"{scatteredFilesUrl}/pkg_version";

View File

@@ -10,12 +10,12 @@ namespace Snap.Hutao.Service.Game.Package;
/// </summary>
[HighQuality]
[DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")]
internal readonly struct ItemOperationInfo
internal readonly struct PackageItemOperationInfo
{
/// <summary>
/// 操作的类型
/// </summary>
public readonly ItemOperationType Type;
public readonly PackageItemOperationType Type;
/// <summary>
/// 目标文件
@@ -33,7 +33,7 @@ internal readonly struct ItemOperationInfo
/// <param name="type">操作类型</param>
/// <param name="remote">远程</param>
/// <param name="local">本地</param>
public ItemOperationInfo(ItemOperationType type, VersionItem remote, VersionItem local)
public PackageItemOperationInfo(PackageItemOperationType type, VersionItem remote, VersionItem local)
{
Type = type;
Remote = remote;

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Game.Package;
/// 包文件操作的类型
/// </summary>
[HighQuality]
internal enum ItemOperationType
internal enum PackageItemOperationType
{
/// <summary>
/// 需要备份

View File

@@ -2,14 +2,13 @@
// Licensed under the MIT license.
using CommunityToolkit.Common;
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包更新状态
/// </summary>
internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
internal sealed class PackageReplaceStatus
{
/// <summary>
/// 构造一个新的包更新状态
@@ -34,10 +33,6 @@ internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
Description = $"{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}";
}
private PackageReplaceStatus()
{
}
public string Name { get; set; } = default!;
/// <summary>
@@ -54,19 +49,4 @@ internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
/// 是否有进度
/// </summary>
public bool IsIndeterminate { get => Percent < 0; }
/// <summary>
/// 克隆
/// </summary>
/// <returns>克隆的实例</returns>
public PackageReplaceStatus Clone()
{
// 进度需要在主线程上创建
return new()
{
Name = Name,
Description = Description,
Percent = Percent,
};
}
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Service.Game.PathAbstraction;
[Injection(InjectAs.Singleton, typeof(IGamePathService))]
internal sealed partial class GamePathService : IGamePathService
{
private readonly IServiceProvider serviceProvider;
private readonly IGameLocatorFactory gameLocatorFactory;
private readonly LaunchOptions launchOptions;
public async ValueTask<ValueResult<bool, string>> SilentGetGamePathAsync()
@@ -17,24 +17,16 @@ internal sealed partial class GamePathService : IGamePathService
// Cannot find in setting
if (string.IsNullOrEmpty(launchOptions.GamePath))
{
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
bool isOk;
string path;
// Try locate by unity log
(isOk, path) = await locatorFactory
.Create(GameLocationSource.UnityLog)
.LocateGamePathAsync()
.ConfigureAwait(false);
(isOk, path) = await gameLocatorFactory.LocateAsync(GameLocationSource.UnityLog).ConfigureAwait(false);
if (!isOk)
{
// Try locate by registry
(isOk, path) = await locatorFactory
.Create(GameLocationSource.Registry)
.LocateGamePathAsync()
.ConfigureAwait(false);
(isOk, path) = await gameLocatorFactory.LocateAsync(GameLocationSource.Registry).ConfigureAwait(false);
}
if (isOk)
@@ -48,13 +40,11 @@ internal sealed partial class GamePathService : IGamePathService
}
}
if (!string.IsNullOrEmpty(launchOptions.GamePath))
{
return new(true, launchOptions.GamePath);
}
else
if (string.IsNullOrEmpty(launchOptions.GamePath))
{
return new(false, default!);
}
return new(true, launchOptions.GamePath);
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Service.Discord;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Game.Unlocker;
@@ -18,6 +19,7 @@ namespace Snap.Hutao.Service.Game.Process;
internal sealed partial class GameProcessService : IGameProcessService
{
private readonly IServiceProvider serviceProvider;
private readonly IProgressFactory progressFactory;
private readonly IDiscordService discordService;
private readonly RuntimeOptions runtimeOptions;
private readonly LaunchOptions launchOptions;
@@ -109,13 +111,13 @@ internal sealed partial class GameProcessService : IGameProcessService
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
// https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html
commandLine = new CommandLineBuilder()
.AppendIf("-popupwindow", launchOptions.IsBorderless)
.AppendIf("-window-mode", launchOptions.IsExclusive, "exclusive")
.AppendIf(launchOptions.IsBorderless, "-popupwindow")
.AppendIf(launchOptions.IsExclusive, "-window-mode", "exclusive")
.Append("-screen-fullscreen", launchOptions.IsFullScreen ? 1 : 0)
.AppendIf("-screen-width", launchOptions.IsScreenWidthEnabled, launchOptions.ScreenWidth)
.AppendIf("-screen-height", launchOptions.IsScreenHeightEnabled, launchOptions.ScreenHeight)
.AppendIf("-monitor", launchOptions.IsMonitorEnabled, launchOptions.Monitor.Value)
.AppendIf("-platform_type CLOUD_THIRD_PARTY_MOBILE", launchOptions.IsUseCloudThirdPartyMobile)
.AppendIf(launchOptions.IsScreenWidthEnabled, "-screen-width", launchOptions.ScreenWidth)
.AppendIf(launchOptions.IsScreenHeightEnabled, "-screen-height", launchOptions.ScreenHeight)
.AppendIf(launchOptions.IsMonitorEnabled, "-monitor", launchOptions.Monitor.Value)
.AppendIf(launchOptions.IsUseCloudThirdPartyMobile, "-platform_type CLOUD_THIRD_PARTY_MOBILE")
.ToString();
}
@@ -138,7 +140,7 @@ internal sealed partial class GameProcessService : IGameProcessService
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
#pragma warning restore CA1859
UnlockTimingOptions options = new(100, 20000, 3000);
Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus)));
IProgress<UnlockerStatus> lockerProgress = progressFactory.CreateForMainThread<UnlockerStatus>(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus)));
return unlocker.UnlockAsync(options, lockerProgress, token);
}

View File

@@ -59,11 +59,11 @@ internal class LaunchScheme : IEquatable<ChannelOptions>
public static bool ExecutableIsOversea(string gameFileName)
{
return gameFileName switch
return gameFileName.ToUpperInvariant() switch
{
GameConstants.GenshinImpactFileName => true,
GameConstants.YuanShenFileName => false,
_ => throw Requires.Fail("无效的游戏可执行文件名称{0}", gameFileName),
GameConstants.GenshinImpactFileNameUpper => true,
GameConstants.YuanShenFileNameUpper => false,
_ => throw Requires.Fail("Invalid game executable file name{0}", gameFileName),
};
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Service.Game.Scheme;
internal static class LaunchSchemeExtension
{
public static SchemeType GetSchemeType(this LaunchScheme scheme)
{
return (scheme.Channel, scheme.IsOversea) switch
{
(ChannelType.Bili, false) => SchemeType.ChineseBilibili,
(_, false) => SchemeType.ChineseOfficial,
(_, true) => SchemeType.Oversea,
};
}
}

View File

@@ -133,23 +133,9 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
private static int IndexOfPattern(in ReadOnlySpan<byte> memory)
{
// E8 ?? ?? ?? ?? 85 C0 7E 07 E8 ?? ?? ?? ?? EB 05
int second = 0;
ReadOnlySpan<byte> secondPart = [0x85, 0xC0, 0x7E, 0x07, 0xE8,];
ReadOnlySpan<byte> thirdPart = [0xEB, 0x05,];
while (second >= 0 && second < memory.Length)
{
second += memory[second..].IndexOf(secondPart);
if (memory[second - 5].Equals(0xE8) && memory.Slice(second + 9, 2).SequenceEqual(thirdPart))
{
return second - 5;
}
second += 5;
}
return -1;
// B9 3C 00 00 00 FF 15
ReadOnlySpan<byte> part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x15];
return memory.IndexOf(part);
}
private static FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out GameModule info)
@@ -241,8 +227,8 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.Size;
nuint rip = localMemoryUserAssemblyAddress + (uint)offset;
rip += *(uint*)(rip + 1) + 5;
rip += *(uint*)(rip + 3) + 7;
rip += 5U;
rip += (nuint)(*(int*)(rip + 2U) + 6);
nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress);
@@ -250,6 +236,8 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0);
rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress;
// CALL or JMP
while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9)
{
rip += (nuint)(*(int*)(rip + 1) + 5);

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Service;
internal static class KnownRegions
{
public static List<NameValue<Region>> Get()
{
return
[
new(SH.WebHoyolabRegionCNGF01, Region.CNGF01),
new(SH.WebHoyolabRegionCNQD01, Region.CNQD01),
new(SH.WebHoyolabRegionOSUSA, Region.OSUSA),
new(SH.WebHoyolabRegionOSEURO, Region.OSEURO),
new(SH.WebHoyolabRegionOSASIA, Region.OSASIA),
new(SH.WebHoyolabRegionOSCHT, Region.OSCHT),
];
}
}

View File

@@ -23,7 +23,7 @@ internal sealed partial class DocumentationProvider : IDocumentationProvider
[typeof(LoginHoyoverseUserPage)] = "https://hut.ao/features/mhy-account-switch.html",
[typeof(LoginMihoyoUserPage)] = "https://hut.ao/features/mhy-account-switch.html",
[typeof(SettingPage)] = "https://hut.ao/features/hutao-settings.html",
[typeof(SpiralAbyssRecordPage)] = "https://hut.ao/features/dashboard.html",
[typeof(SpiralAbyssRecordPage)] = "https://hut.ao/features/hutao-API.html",
[typeof(TestPage)] = Home,
[typeof(WikiAvatarPage)] = "https://hut.ao/features/character-wiki.html",
[typeof(WikiMonsterPage)] = "https://hut.ao/features/monster-wiki.html",

View File

@@ -13,8 +13,10 @@ internal static class SupportedCultures
ToNameValue(CultureInfo.GetCultureInfo("zh-Hans")),
ToNameValue(CultureInfo.GetCultureInfo("zh-Hant")),
ToNameValue(CultureInfo.GetCultureInfo("en")),
ToNameValue(CultureInfo.GetCultureInfo("ko")),
ToNameValue(CultureInfo.GetCultureInfo("ru")),
ToNameValue(CultureInfo.GetCultureInfo("ja")),
ToNameValue(CultureInfo.GetCultureInfo("id")),
ToNameValue(CultureInfo.GetCultureInfo("ko")),
];
public static List<NameValue<CultureInfo>> Get()

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Update;
namespace Snap.Hutao.Service.Abstraction;
internal interface IUpdateService
{
ValueTask<bool> CheckForUpdateAndDownloadAsync(IProgress<UpdateStatus> progress, CancellationToken token = default);
ValueTask LaunchUpdaterAsync();
}

View File

@@ -0,0 +1,143 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.IO.Http.Sharding;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Response;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using Windows.Storage;
namespace Snap.Hutao.Service.Update;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IUpdateService))]
internal sealed partial class UpdateService : IUpdateService
{
private const string UpdaterFilename = "Snap.Hutao.Deployment.exe";
private readonly IServiceProvider serviceProvider;
public async ValueTask<bool> CheckForUpdateAndDownloadAsync(IProgress<UpdateStatus> progress, CancellationToken token = default)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
ITaskContext taskContext = scope.ServiceProvider.GetRequiredService<ITaskContext>();
await taskContext.SwitchToBackgroundAsync();
HutaoInfrastructureClient infrastructureClient = serviceProvider.GetRequiredService<HutaoInfrastructureClient>();
HutaoResponse<HutaoVersionInformation> response = await infrastructureClient.GetHutaoVersionInfomationAsync(token).ConfigureAwait(false);
if (!response.IsOk())
{
return false;
}
HutaoVersionInformation versionInformation = response.Data;
string msixPath = GetUpdatePackagePath();
if (!LocalSetting.Get(SettingKeys.OverrideUpdateVersionComparison, false))
{
if (scope.ServiceProvider.GetRequiredService<RuntimeOptions>().Version >= versionInformation.Version)
{
if (File.Exists(msixPath))
{
File.Delete(msixPath);
}
return false;
}
}
progress.Report(new(versionInformation.Version.ToString(), 0, 0));
if (versionInformation.Sha256 is not { Length: > 0 } sha256)
{
return false;
}
if (File.Exists(msixPath) && await CheckUpdateCacheSHA256Async(msixPath, sha256, token).ConfigureAwait(false))
{
return true;
}
return await DownloadUpdatePackageAsync(versionInformation, msixPath, progress, token).ConfigureAwait(false);
}
}
public async ValueTask LaunchUpdaterAsync()
{
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
string updaterTargetPath = runtimeOptions.GetDataFolderUpdateCacheFolderFile(UpdaterFilename);
Uri updaterSourceUri = $"ms-appx:///{UpdaterFilename}".ToUri();
StorageFile updaterFile = await StorageFile.GetFileFromApplicationUriAsync(updaterSourceUri);
await updaterFile.OverwriteCopyAsync(updaterTargetPath).ConfigureAwait(false);
string commandLine = new CommandLineBuilder()
.Append("--package-path", GetUpdatePackagePath(runtimeOptions))
.Append("--family-name", runtimeOptions.FamilyName)
.Append("--update-behavior", true)
.ToString();
Process.Start(new ProcessStartInfo()
{
Arguments = commandLine,
WindowStyle = ProcessWindowStyle.Minimized,
FileName = updaterTargetPath,
UseShellExecute = true,
});
}
private static async ValueTask<bool> CheckUpdateCacheSHA256Async(string filePath, string remoteHash, CancellationToken token = default)
{
string localHash = await SHA256.HashFileAsync(filePath, token).ConfigureAwait(false);
return string.Equals(localHash, remoteHash, StringComparison.OrdinalIgnoreCase);
}
private string GetUpdatePackagePath(RuntimeOptions? runtimeOptions = default)
{
runtimeOptions ??= serviceProvider.GetRequiredService<RuntimeOptions>();
return runtimeOptions.GetDataFolderUpdateCacheFolderFile("Snap.Hutao.msix");
}
private async ValueTask<bool> DownloadUpdatePackageAsync(HutaoVersionInformation versionInformation, string filePath, IProgress<UpdateStatus> progress, CancellationToken token = default)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
using (HttpClient httpClient = scope.ServiceProvider.GetRequiredService<HttpClient>())
{
string version = versionInformation.Version.ToString();
foreach (string url in versionInformation.Urls)
{
HttpShardCopyWorkerOptions<UpdateStatus> options = new()
{
HttpClient = httpClient,
SourceUrl = url,
DestinationFilePath = filePath,
StatusFactory = (bytesRead, totalBytes) => new UpdateStatus(version, bytesRead, totalBytes),
};
using (HttpShardCopyWorker<UpdateStatus> worker = await HttpShardCopyWorker<UpdateStatus>.CreateAsync(options).ConfigureAwait(false))
{
await worker.CopyAsync(progress, token).ConfigureAwait(false);
}
string? remoteHash = versionInformation.Sha256;
ArgumentNullException.ThrowIfNull(remoteHash);
if (await CheckUpdateCacheSHA256Async(filePath, remoteHash, token).ConfigureAwait(false))
{
return true;
}
}
}
}
return false;
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Common;
namespace Snap.Hutao.Service.Update;
internal sealed class UpdateStatus
{
public UpdateStatus(string version, long bytesRead, long totalBytes)
{
Version = version;
VersionDescription = SH.FormatServiceUpdateStatusVersionDescription(Version);
BytesRead = bytesRead;
TotalBytes = totalBytes;
ProgressDescription = bytesRead != totalBytes
? $"{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}"
: string.Empty;
}
public string? Version { get; set; }
public string VersionDescription { get; }
public double BytesRead { get; set; }
public double TotalBytes { get; set; }
public string ProgressDescription { get; }
}

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Service.User;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IUserCollectionService))]
internal sealed partial class UserCollectionService : IUserCollectionService
internal sealed partial class UserCollectionService : IUserCollectionService, IDisposable
{
private readonly ScopedDbCurrent<BindingUser, Model.Entity.User, UserChangedMessage> dbCurrent;
private readonly IUserInitializationService userInitializationService;
@@ -22,7 +22,7 @@ internal sealed partial class UserCollectionService : IUserCollectionService
private readonly ITaskContext taskContext;
private readonly IMessenger messenger;
private readonly Throttler throttler = new();
private readonly SemaphoreSlim throttler = new(1);
private ObservableCollection<BindingUser>? userCollection;
private Dictionary<string, BindingUser>? midUserMap;
@@ -38,7 +38,9 @@ internal sealed partial class UserCollectionService : IUserCollectionService
public async ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync()
{
using (await throttler.ThrottleAsync().ConfigureAwait(false))
// Force run in background thread, otherwise will cause reentrance
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
using (await throttler.EnterAsync().ConfigureAwait(false))
{
if (userCollection is null)
{
@@ -131,17 +133,7 @@ internal sealed partial class UserCollectionService : IUserCollectionService
return default;
}
try
{
return uidUserGameRoleMap[uid];
}
catch (InvalidOperationException)
{
// Sequence contains more than one matching element
// TODO: return a specialize UserGameRole to indicate error
}
return default;
return uidUserGameRoleMap.GetValueOrDefault(uid);
}
public bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user)
@@ -186,4 +178,9 @@ internal sealed partial class UserCollectionService : IUserCollectionService
ArgumentNullException.ThrowIfNull(newUser.UserInfo);
return new(UserOptionResult.Added, newUser.UserInfo.Uid);
}
public void Dispose()
{
throttler.Dispose();
}
}

View File

@@ -204,8 +204,10 @@
<AdditionalFiles Include="stylecop.json" />
<AdditionalFiles Include="Resource\Localization\SH.resx" />
<AdditionalFiles Include="Resource\Localization\SH.en.resx" />
<AdditionalFiles Include="Resource\Localization\SH.id.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ja.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ko.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ru.resx" />
<AdditionalFiles Include="Resource\Localization\SH.zh-Hant.resx" />
</ItemGroup>
@@ -301,11 +303,19 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.1" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.5.0" />
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.9.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -67,7 +67,7 @@
VerticalAlignment="Bottom">
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding GameAccounts}"
ItemsSource="{Binding GameAccountsView}"
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>

View File

@@ -8,6 +8,7 @@
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shmm="using:Snap.Hutao.Model.Metadata"
Visibility="{x:Bind SelectedItem.Parameters.Count, Converter={StaticResource Int32ToVisibilityConverter}, Mode=OneWay}"
mc:Ignorable="d">
<UserControl.Resources>
<Thickness x:Key="SettingsCardPadding">16,8</Thickness>
@@ -29,8 +30,7 @@
Header="{shcm:ResourceString Name=ViewControlBaseValueSliderLevel}"
IsExpanded="True"
ItemTemplate="{StaticResource ParameterDescriptionTemplate}"
ItemsSource="{x:Bind SelectedItem.Parameters, Mode=OneWay}"
Visibility="{x:Bind SelectedItem.Parameters.Count, Converter={StaticResource Int32ToVisibilityConverter}, Mode=OneWay}">
ItemsSource="{x:Bind SelectedItem.Parameters, Mode=OneWay}">
<shc:SizeRestrictedContentControl Margin="0,-8">
<ComboBox
x:Name="LevelSelectorComboBox"

View File

@@ -76,16 +76,15 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
private async ValueTask InitializeAsync()
{
if (isInitializingOrInitialized)
if (!isInitializingOrInitialized)
{
return;
isInitializingOrInitialized = true;
await WebView.EnsureCoreWebView2Async();
WebView.CoreWebView2.DisableDevToolsForReleaseBuild();
WebView.CoreWebView2.DocumentTitleChanged += documentTitleChangedEventHander;
}
isInitializingOrInitialized = true;
await WebView.EnsureCoreWebView2Async();
WebView.CoreWebView2.DisableDevToolsForReleaseBuild();
WebView.CoreWebView2.DocumentTitleChanged += documentTitleChangedEventHander;
RefreshWebview2Content();
}
@@ -128,6 +127,9 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
string source = SourceProvider.GetSource(userAndUid);
if (!string.IsNullOrEmpty(source))
{
CoreWebView2Navigator navigator = new(coreWebView2);
await navigator.NavigateAsync("about:blank").ConfigureAwait(true);
try
{
await coreWebView2.Profile.ClearBrowsingDataAsync();
@@ -138,9 +140,6 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
await coreWebView2.DeleteCookiesAsync(userAndUid.IsOversea).ConfigureAwait(true);
}
CoreWebView2Navigator navigator = new(coreWebView2);
await navigator.NavigateAsync("about:blank").ConfigureAwait(true);
coreWebView2
.SetCookie(user.CookieToken, user.LToken, userAndUid.IsOversea)
.SetMobileUserAgent(userAndUid.IsOversea);

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Converters;
using Snap.Hutao.Control;
namespace Snap.Hutao.View.Converter;

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Control;
namespace Snap.Hutao.View.Converter;
[DependencyProperty("VisibleValue", typeof(object))]
[DependencyProperty("CollapsedValue", typeof(object))]
internal sealed partial class VisibilityToObjectConverter : DependencyValueConverter<Visibility, object>
{
public override object Convert(Visibility from)
{
return from is Visibility.Visible ? VisibleValue : CollapsedValue;
}
}

View File

@@ -12,6 +12,7 @@ using Snap.Hutao.Web.Response;
using System.Collections.Specialized;
using System.IO;
using System.Web;
using Windows.Foundation;
namespace Snap.Hutao.View.Dialog;

View File

@@ -42,7 +42,12 @@
VerticalAlignment="Top"
cw:VisualExtensions.NormalizedCenterPoint="0.5">
<cww:ConstrainedBox AspectRatio="1080:390" CornerRadius="{ThemeResource ControlCornerRadiusTop}">
<shci:CachedImage Source="{Binding Banner}" Stretch="UniformToFill"/>
<shci:CachedImage
VerticalAlignment="Center"
PlaceholderMargin="16"
PlaceholderSource="{StaticResource UI_EmotionIcon271}"
Source="{Binding Banner}"
Stretch="UniformToFill"/>
</cww:ConstrainedBox>
<cwa:Explicit.Animations>
<cwa:AnimationSet x:Name="ImageZoomInAnimation">
@@ -171,7 +176,7 @@
Message="{Binding Content}"
Severity="{Binding Severity}">
<StackPanel Margin="0,0,0,8" Orientation="Horizontal">
<HyperlinkButton Content="查看详情" NavigateUri="{Binding Link}"/>
<HyperlinkButton Content="{shcm:ResourceString Name=ViewPageAnnouncementViewDetails}" NavigateUri="{Binding Link}"/>
<TextBlock
Margin="8,0,0,2"
VerticalAlignment="Center"
@@ -235,4 +240,4 @@
</StackPanel>
</ScrollViewer>
</Grid>
</shc:ScopedPage>
</shc:ScopedPage>

View File

@@ -7,12 +7,11 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
xmlns:mxim="using:Microsoft.Xaml.Interactions.Media"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shccs="using:Snap.Hutao.Control.Collection.Selector"
xmlns:shch="using:Snap.Hutao.Control.Helper"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shvc="using:Snap.Hutao.View.Control"
xmlns:shvg="using:Snap.Hutao.ViewModel.Game"
@@ -197,7 +196,7 @@
<Border Style="{StaticResource BorderCardStyle}">
<ListView
ItemTemplate="{StaticResource GameAccountListTemplate}"
ItemsSource="{Binding GameAccounts}"
ItemsSource="{Binding GameAccountsView}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</Border>
@@ -207,8 +206,7 @@
shch:SettingsExpanderHelper.IsItemsEnabled="{Binding LaunchOptions.IsEnabled}"
Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE943;}"
IsExpanded="True">
HeaderIcon="{shcm:FontIcon Glyph=&#xE943;}">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsEnabled, Mode=TwoWay}"/>
<cwc:SettingsExpander.Items>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceExclusiveDescription}" Header="-window-mode exclusive">
@@ -346,10 +344,20 @@
</Grid>
<Grid Visibility="{Binding GamePathSelectedAndValid, Converter={StaticResource BoolToVisibilityRevertConverter}}">
<StackPanel
MaxWidth="600"
Margin="128,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="3">
<shci:CachedImage
Width="120"
Height="120"
EnableLazyLoading="False"
Source="{StaticResource UI_EmotionIcon445}"/>
<TextBlock
Margin="0,5,0,21"
HorizontalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageLaunchGameSelectGamePath}"/>
<Border Style="{ThemeResource BorderCardStyle}">
<ListView
ItemTemplate="{StaticResource GamePathEntryListTemplate}"
@@ -364,9 +372,7 @@
HeaderIcon="{shcm:FontIcon Glyph=&#xE7FC;}"
IsClickEnabled="True">
<cwc:SettingsCard.Description>
<StackPanel>
<TextBlock Foreground="{ThemeResource SystemErrorTextColor}" Text="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"/>
</StackPanel>
<TextBlock Foreground="{ThemeResource SystemErrorTextColor}" Text="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"/>
</cwc:SettingsCard.Description>
</cwc:SettingsCard>
</StackPanel>

View File

@@ -23,7 +23,10 @@
<ScrollViewer shch:ScrollViewerHelper.LeftPanelMaxWidth="800" Style="{StaticResource TwoPanelScrollViewerStyle}">
<shch:ScrollViewerHelper.RightPanel>
<StackPanel Width="360" Margin="0,16,16,16">
<StackPanel
Width="360"
Margin="0,16,16,16"
Spacing="{StaticResource SettingsCardSpacing}">
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Grid Style="{ThemeResource GridCardStyle}">
<Border
@@ -51,16 +54,6 @@
ColumnSpacing="8"
Columns="2"
RowSpacing="8">
<HyperlinkButton
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Command="{Binding UpdateCheckCommand}"
Content="{shcm:ResourceString Name=ViewPageSettingUpdateCheckAction}"/>
<HyperlinkButton
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Command="{Binding StoreReviewCommand}"
Content="{shcm:ResourceString Name=ViewPageSettingStoreReviewNavigate}"/>
<HyperlinkButton
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
@@ -93,6 +86,27 @@
</Grid>
</Grid>
</Border>
<cwc:SettingsExpander
Header="{shcm:ResourceString Name=ViewPageSettingElevatedModeHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE7EF;}"
IsExpanded="True">
<cwc:SettingsExpander.Items>
<cwc:SettingsCard
Command="{Binding RestartAsElevatedCommand}"
Description="{shcm:ResourceString Name=ViewPageSettingElevatedModeDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingElevatedModeRestartAction}"
IsClickEnabled="True"
IsEnabled="{Binding HutaoOptions.IsElevated, Converter={StaticResource BoolNegationConverter}}"/>
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutAction}"
Command="{Binding CreateDesktopShortcutCommand}"
Description="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutHeader}"
IsClickEnabled="True"/>
</cwc:SettingsExpander.Items>
</cwc:SettingsExpander>
</StackPanel>
</shch:ScrollViewerHelper.RightPanel>
<Grid Padding="16" HorizontalAlignment="Left">
@@ -146,17 +160,17 @@
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperHeader}"
Visibility="{Binding UserOptions.IsLicensedDeveloper, Converter={StaticResource BoolToVisibilityConverter}}"/>
<cwc:SettingsCard
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerHeader}"
Visibility="{Binding UserOptions.IsMaintainer, Converter={StaticResource BoolToVisibilityConverter}}">
Visibility="{Binding UserOptions.IsLicensedDeveloper, Converter={StaticResource BoolToVisibilityConverter}}">
<Button
Command="{Binding OpenTestPageCommand}"
Content="TEST"
Style="{ThemeResource SettingButtonStyle}"/>
</cwc:SettingsCard>
<cwc:SettingsCard
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerHeader}"
Visibility="{Binding UserOptions.IsMaintainer, Converter={StaticResource BoolToVisibilityConverter}}"/>
<cwc:SettingsCard Description="{Binding UserOptions.GachaLogExpireAtSlim}" Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportGachaLogExpiredAtHeader}"/>
<cwc:SettingsCard
Command="{Binding Passport.OpenRedeemWebsiteCommand}"
@@ -191,15 +205,6 @@
HeaderIcon="{shcm:FontIcon Glyph=&#xE776;}"
IsClickEnabled="True"/>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingShellExperienceHeader}"/>
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutAction}"
Command="{Binding CreateDesktopShortcutCommand}"
Description="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE7EF;}"
IsClickEnabled="True"/>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingApperanceHeader}"/>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageSettingApperanceLanguageDescription}"
@@ -229,35 +234,47 @@
Description="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE92E;}">
<StackPanel Orientation="Horizontal">
<cwc:UniformGrid
Margin="16,-12"
ColumnSpacing="16"
Columns="2"
Orientation="Horizontal"
RowSpacing="0">
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Win"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasWindows, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Ctrl"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasControl, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Shift"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasShift, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Alt"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasAlt, Mode=TwoWay}"/>
</cwc:UniformGrid>
<shc:SizeRestrictedContentControl>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
MinWidth="32"
MinHeight="32"
Padding="0"
VerticalAlignment="Center"
Content="&#xEDA7;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Style="{ThemeResource SettingButtonStyle}">
<Button.Flyout>
<Flyout FlyoutPresenterStyle="{ThemeResource FlyoutPresenterPadding16And10Style}">
<cwc:UniformGrid
ColumnSpacing="16"
Columns="2"
Orientation="Horizontal"
RowSpacing="0">
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Win"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasWindows, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Ctrl"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasControl, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Shift"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasShift, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Alt"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasAlt, Mode=TwoWay}"/>
</cwc:UniformGrid>
</Flyout>
</Button.Flyout>
</Button>
<shc:SizeRestrictedContentControl VerticalAlignment="Center">
<ComboBox
MinWidth="120"
VerticalAlignment="Center"
@@ -305,22 +322,19 @@
</cwc:SettingsCard>
</cwc:SettingsExpander.Items>
</cwc:SettingsExpander>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageSettingHomeAnnouncementRegionDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingHomeAnnouncementRegionHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE8E4;}">
<shc:SizeRestrictedContentControl>
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding AppOptions.LazyRegions.Value}"
SelectedItem="{Binding SelectedRegion, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
</cwc:SettingsCard>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingGameHeader}"/>
<cwc:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingSetGamePathAction}"
Command="{Binding SetPowerShellPathCommand}"
Header="{shcm:ResourceString Name=ViewPageSettingSetPowerShellPathHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE756;}"
IsClickEnabled="True">
<cwc:SettingsCard.Description>
<StackPanel>
<TextBlock Text="{shcm:ResourceString Name=ViewPageSettingSetPowerShellDescription}"/>
<TextBlock Text="{Binding AppOptions.PowerShellPath}"/>
</StackPanel>
</cwc:SettingsCard.Description>
</cwc:SettingsCard>
<cwc:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingDeleteCacheAction}"

View File

@@ -78,15 +78,17 @@
</cwc:SettingsCard>
<cwc:SettingsCard Header="Reset Guide State">
<StackPanel Orientation="Horizontal">
<Button Command="{Binding ResetGuideStateCommand}" Content="Reset (No restart)"/>
</StackPanel>
<Button
Command="{Binding ResetGuideStateCommand}"
Content="Reset (No restart)"
Style="{ThemeResource SettingButtonStyle}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Header="Resize MainWindow">
<StackPanel Orientation="Horizontal">
<Button Command="{Binding ResetMainWindowSizeCommand}" Content="Reset"/>
</StackPanel>
<Button
Command="{Binding ResetMainWindowSizeCommand}"
Content="Reset"
Style="{ThemeResource SettingButtonStyle}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Header="Suppress Metadata Initialization">
@@ -97,6 +99,10 @@
<ToggleSwitch IsOn="{Binding OverrideElevationRequirement, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Header="Override Update Version Comparison">
<ToggleSwitch IsOn="{Binding OverrideUpdateVersionComparison, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard
Command="{Binding CompensationGachaLogServiceTimeCommand}"
Header="Compensation GachaLog Service Time For 15 Days"

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