Compare commits

..

11 Commits

Author SHA1 Message Date
Masterain
db0d6d4bc6 New translations sh.resx (English) 2023-12-20 23:00:38 -08:00
Masterain
c0c2dd535e New translations sh.resx (English) 2023-12-20 22:55:48 -08:00
Masterain
deb49fe26a New translations sh.resx (English) 2023-12-20 18:38:42 -08:00
Masterain
c5191b0a1c New translations sh.resx (Chinese Traditional) 2023-12-20 18:38:41 -08:00
Masterain
9efc24dadd New translations sh.resx (Russian) 2023-12-20 18:38:40 -08:00
Masterain
b68e696fc3 New translations sh.resx (Korean) 2023-12-20 18:38:39 -08:00
Masterain
a68c461197 New translations sh.resx (Japanese) 2023-12-20 18:38:38 -08:00
Masterain
4ec727ab6b New translations sh.resx (Indonesian) 2023-12-20 18:38:37 -08:00
Masterain
b407edb979 New translations sh.resx (Indonesian) 2023-12-19 18:24:19 -08:00
Masterain
0d83c4bb6d New translations sh.resx (Indonesian) 2023-12-18 18:09:58 -08:00
Masterain
14e0d59e56 New translations sh.resx (Indonesian) 2023-12-17 18:11:49 -08:00
94 changed files with 996 additions and 1739 deletions

View File

@@ -57,9 +57,7 @@ jobs:
> 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
> [!IMPORTANT]
> 请注意,从 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) 以安装测试版安装包
> 请安装 [Snap.Hutao.CI.cer](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate/Snap.Hutao.CI.cer) 以安装测试版安装包
"
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=\"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, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=DGP Studio CI\"");
content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\"");
}
else if (AppVeyor.IsRunningOnAppVeyor)

View File

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

View File

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

View File

@@ -9,6 +9,10 @@ using Snap.Hutao.ViewModel.Abstraction;
namespace Snap.Hutao.Control;
/// <summary>
/// 表示支持取消加载的异步页面
/// 在被导航到其他页面前触发取消异步通知
/// </summary>
[HighQuality]
[SuppressMessage("", "CA1001")]
internal class ScopedPage : Page
@@ -17,8 +21,9 @@ internal class ScopedPage : Page
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope currentScope;
private bool inFrame = true;
/// <summary>
/// 构造一个新的页面
/// </summary>
protected ScopedPage()
{
unloadEventHandler = OnUnloaded;
@@ -26,6 +31,11 @@ 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)
@@ -51,32 +61,6 @@ 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)
{
@@ -95,4 +79,19 @@ 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,10 +18,4 @@
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

@@ -31,5 +31,4 @@
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.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

@@ -1,23 +0,0 @@
// 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; }
public int BytesRead { get; set; }
}
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 > 1000 || totalBytesRead == contentLength)
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
{
lock (syncRoot)
{
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000 || totalBytesRead == contentLength)
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
{
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 > 1000)
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
{
progress.Report(statusFactory(totalBytesRead));
stopwatch = ValueStopwatch.StartNew();

View File

@@ -2,8 +2,6 @@
// 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;
@@ -17,17 +15,7 @@ internal sealed class ConsoleWindowLifeTime : IDisposable
if (LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, false))
{
consoleWindowAllocated = AllocConsole();
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");
}
SetConsoleTitle("Snap Hutao Debug Console");
}
}

View File

@@ -1,16 +0,0 @@
// 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 IsInfoBarToggleChecked = "IsInfoBarToggleChecked";
public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds";
#endregion
#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
#region Passport
public const string PassportUserName = "PassportUserName";
public const string PassportPassword = "PassportPassword";
#endregion
#region Cultivation
public const string PassportPassword = "PassportPassword";
public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked";
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds";
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
public const string CultivationAvatarLevelCurrent = "CultivationAvatarLevelCurrent";
public const string CultivationAvatarLevelTarget = "CultivationAvatarLevelTarget";
public const string CultivationAvatarSkillACurrent = "CultivationAvatarSkillACurrent";
@@ -43,18 +43,13 @@ 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
#region DevTool
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
#endregion
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
}

View File

@@ -1,6 +1,7 @@
// 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;
@@ -18,16 +19,23 @@ 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);
await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false);
using (Stream inputStream = (await iconFile.OpenReadAsync()).AsStream())
{
using (FileStream outputStream = File.Create(targetLogoPath))
{
await inputStream.CopyToAsync(outputStream).ConfigureAwait(false);
}
}
}
catch
{
@@ -37,15 +45,12 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink);
Marshal.ThrowExceptionForHR(result);
shellLink.SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
shellLink.SetPath(appOptions.PowerShellPath);
shellLink.SetArguments($"""
-Command "Start-Process shell:AppsFolder\{runtimeOptions.FamilyName}!App -verb runas"
""");
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_SHOWMINNOACTIVE);
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,24 +1,45 @@
// 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 DispatcherQueue dispatcherQueue;
private readonly Action<T> handler;
private readonly SynchronizationContext synchronizationContext;
private readonly Action<T>? handler;
private readonly SendOrPostCallback invokeHandlers;
public DispatcherQueueProgress(Action<T> handler, DispatcherQueue dispatcherQueue)
public DispatcherQueueProgress(Action<T> handler, SynchronizationContext synchronizationContext)
{
this.dispatcherQueue = dispatcherQueue;
this.synchronizationContext = synchronizationContext;
invokeHandlers = new SendOrPostCallback(InvokeHandlers);
ArgumentNullException.ThrowIfNull(handler);
this.handler = handler;
}
public event EventHandler<T>? ProgressChanged;
public void Report(T value)
{
Action<T> handler = this.handler;
dispatcherQueue.TryEnqueue(() => handler(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);
}
}

View File

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

View File

@@ -1,11 +0,0 @@
// 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, ITaskContextUnsafe
internal sealed class TaskContext : ITaskContext
{
private readonly DispatcherQueueSynchronizationContext synchronizationContext;
private readonly DispatcherQueue dispatcherQueue;
@@ -24,7 +24,7 @@ internal sealed class TaskContext : ITaskContext, ITaskContextUnsafe
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
}
public DispatcherQueue DispatcherQueue { get => dispatcherQueue; }
public SynchronizationContext SynchronizationContext { get => synchronizationContext; }
/// <inheritdoc/>
public ThreadPoolSwitchOperation SwitchToBackgroundAsync()

View File

@@ -0,0 +1,19 @@
// 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,20 +2,12 @@
// 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

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

View File

@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Windows.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao;
@@ -19,6 +20,9 @@ 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>
/// 构造一个新的主窗体
@@ -29,6 +33,13 @@ 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/>
@@ -40,4 +51,13 @@ 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

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

View File

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

View File

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

View File

@@ -995,9 +995,6 @@
<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>
@@ -1652,9 +1649,6 @@
<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>
@@ -2624,12 +2618,6 @@
<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>
@@ -2886,7 +2874,7 @@
<value>Oversea Server: Asian</value>
</data>
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
<value>Oversea Server: TW/HK/MU server</value>
<value>International Server Taiwan Server</value>
</data>
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
<value>Oversea Server: EU</value>

File diff suppressed because it is too large Load Diff

View File

@@ -995,9 +995,6 @@
<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>
@@ -1652,9 +1649,6 @@
<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>
@@ -2624,12 +2618,6 @@
<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>
@@ -2886,7 +2874,7 @@
<value>国际服 亚服</value>
</data>
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
<value>国际服 港澳台服</value>
<value>国际服 台服</value>
</data>
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
<value>国际服 欧服</value>

View File

@@ -995,9 +995,6 @@
<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>
@@ -1652,9 +1649,6 @@
<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>
@@ -2624,12 +2618,6 @@
<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>
@@ -2886,7 +2874,7 @@
<value>国际服 亚服</value>
</data>
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
<value>国际服 港澳台服</value>
<value>国际服 台服</value>
</data>
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
<value>国际服 欧服</value>

View File

@@ -120,12 +120,6 @@
<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>
@@ -192,6 +186,9 @@
<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>
@@ -935,6 +932,9 @@
<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,9 +995,6 @@
<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>
@@ -1577,12 +1574,6 @@
<data name="ViewModelSettingGeetestCustomUrlSucceed" xml:space="preserve">
<value>无感验证复合 Url 配置成功</value>
</data>
<data name="ViewModelSettingNotRunningInElevatedMode" xml:space="preserve">
<value>当前以用户身份运行</value>
</data>
<data name="ViewModelSettingRunningInElevatedMode" xml:space="preserve">
<value>当前以管理员身份运行</value>
</data>
<data name="ViewModelSettingSetDataFolderSuccess" xml:space="preserve">
<value>设置数据目录成功,重启以应用更改</value>
</data>
@@ -1658,9 +1649,6 @@
<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>
@@ -2150,9 +2138,6 @@
<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>
@@ -2241,7 +2226,7 @@
<value>创建</value>
</data>
<data name="ViewPageSettingCreateDesktopShortcutDescription" xml:space="preserve">
<value>在桌面上创建默认以管理员身份启动的快捷方式</value>
<value>在桌面上创建默认以管理员方式启动的快捷方式</value>
</data>
<data name="ViewPageSettingCreateDesktopShortcutHeader" xml:space="preserve">
<value>创建快捷方式</value>
@@ -2285,15 +2270,6 @@
<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>
@@ -2330,12 +2306,6 @@
<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>
@@ -2450,6 +2420,12 @@
<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>
@@ -2636,12 +2612,6 @@
<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>
@@ -2753,15 +2723,6 @@
<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>
@@ -2891,30 +2852,9 @@
<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

@@ -995,9 +995,6 @@
<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>
@@ -1652,9 +1649,6 @@
<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>
@@ -2624,12 +2618,6 @@
<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>
@@ -2886,7 +2874,7 @@
<value>国际服 亚服</value>
</data>
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
<value>国际服 港澳台服</value>
<value>国际服 台服</value>
</data>
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
<value>国际服 欧服</value>

View File

@@ -995,9 +995,6 @@
<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>
@@ -1652,9 +1649,6 @@
<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>
@@ -2624,12 +2618,6 @@
<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>
@@ -2886,7 +2874,7 @@
<value>国际服 亚服</value>
</data>
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
<value>国际服 港澳台服</value>
<value>国际服 台服</value>
</data>
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
<value>国际服 欧服</value>

View File

@@ -1,7 +1,6 @@
// 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;
@@ -15,9 +14,7 @@ internal interface IAnnouncementService
/// <summary>
/// 异步获取游戏公告与活动,通常会进行缓存
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <param name="region">服务器</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>公告包装器</returns>
ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken cancellationToken = default);
ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(CancellationToken cancellationToken = default);
}

View File

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

View File

@@ -1,12 +1,13 @@
// 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;
@@ -14,12 +15,41 @@ 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);
@@ -42,14 +72,6 @@ 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,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Model;
using Snap.Hutao.Web.Hoyolab;
using System.Globalization;
namespace Snap.Hutao.Service;
@@ -13,9 +12,4 @@ 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 = entryList.ToObservableCollection();
entries = new(entryList);
}
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(entry.Uid, dailyNote).ConfigureAwait(false);
await dailyNoteWebhookOperation.TryPostDailyNoteToWebhookAsync(dailyNote).ConfigureAwait(false);
}
}
}

View File

@@ -2,7 +2,6 @@
// 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;
@@ -19,7 +18,7 @@ internal sealed partial class DailyNoteWebhookOperation
private readonly DailyNoteOptions dailyNoteOptions;
private readonly HttpClient httpClient;
public async ValueTask TryPostDailyNoteToWebhookAsync(PlayerUid playerUid, WebDailyNote dailyNote, CancellationToken token = default)
public async ValueTask TryPostDailyNoteToWebhookAsync(WebDailyNote dailyNote, CancellationToken token = default)
{
string? targetUrl = dailyNoteOptions.WebhookUrl;
if (string.IsNullOrEmpty(targetUrl) || !Uri.TryCreate(targetUrl, UriKind.Absolute, out Uri? targetUri))
@@ -29,7 +28,6 @@ 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

@@ -16,6 +16,7 @@ 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;
@@ -89,7 +90,12 @@ internal sealed partial class GameAccountService : IGameAccountService
public bool SetGameAccount(GameAccount account)
{
return RegistryInterop.Set(account);
if (string.IsNullOrEmpty(appOptions.PowerShellPath))
{
ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!);
}
return RegistryInterop.Set(account, appOptions.PowerShellPath);
}
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)

View File

@@ -2,7 +2,10 @@
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
@@ -17,16 +20,48 @@ internal static class RegistryInterop
private const string GenshinKey = $@"HKEY_CURRENT_USER\{GenshinPath}";
private const string SdkChineseKey = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
public static bool Set(GameAccount? account)
/// <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)
{
if (account is not null)
{
// 存回注册表的字节需要 '\0' 结尾
byte[] target = [.. Encoding.UTF8.GetBytes(account.MihoyoSDK), 0];
Registry.SetValue(GenshinKey, SdkChineseKey, target);
Encoding.UTF8.GetByteCount(account.MihoyoSDK);
byte[] tempBytes = Encoding.UTF8.GetBytes(account.MihoyoSDK);
byte[] target = new byte[tempBytes.Length + 1];
tempBytes.CopyTo(target, 0);
string? get = Get();
if (get == account.MihoyoSDK)
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)
{
return true;
}
@@ -35,6 +70,10 @@ internal static class RegistryInterop
return false;
}
/// <summary>
/// 在注册表中获取账号信息
/// </summary>
/// <returns>当前注册表中的信息</returns>
public static unsafe string? Get()
{
object? sdk = Registry.GetValue(GenshinKey, SdkChineseKey, Array.Empty<byte>());

View File

@@ -133,9 +133,23 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
private static int IndexOfPattern(in ReadOnlySpan<byte> memory)
{
// B9 3C 00 00 00 FF 15
ReadOnlySpan<byte> part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x15];
return memory.IndexOf(part);
// 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;
}
private static FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out GameModule info)
@@ -227,8 +241,8 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
nuint localMemoryUserAssemblyAddress = localMemoryUnityPlayerAddress + unityPlayer.Size;
nuint rip = localMemoryUserAssemblyAddress + (uint)offset;
rip += 5U;
rip += (nuint)(*(int*)(rip + 2) + 6);
rip += *(uint*)(rip + 1) + 5;
rip += *(uint*)(rip + 3) + 7;
nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress);

View File

@@ -1,23 +0,0 @@
// 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

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

View File

@@ -1,13 +0,0 @@
// 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

@@ -1,143 +0,0 @@
// 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

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

View File

@@ -204,10 +204,8 @@
<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>
@@ -308,10 +306,6 @@
<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.5.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

@@ -8,7 +8,6 @@
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>
@@ -30,7 +29,8 @@
Header="{shcm:ResourceString Name=ViewControlBaseValueSliderLevel}"
IsExpanded="True"
ItemTemplate="{StaticResource ParameterDescriptionTemplate}"
ItemsSource="{x:Bind SelectedItem.Parameters, Mode=OneWay}">
ItemsSource="{x:Bind SelectedItem.Parameters, Mode=OneWay}"
Visibility="{x:Bind SelectedItem.Parameters.Count, Converter={StaticResource Int32ToVisibilityConverter}, Mode=OneWay}">
<shc:SizeRestrictedContentControl Margin="0,-8">
<ComboBox
x:Name="LevelSelectorComboBox"

View File

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

View File

@@ -1,17 +0,0 @@
// 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

@@ -171,7 +171,7 @@
Message="{Binding Content}"
Severity="{Binding Severity}">
<StackPanel Margin="0,0,0,8" Orientation="Horizontal">
<HyperlinkButton Content="{shcm:ResourceString Name=ViewPageAnnouncementViewDetails}" NavigateUri="{Binding Link}"/>
<HyperlinkButton Content="查看详情" NavigateUri="{Binding Link}"/>
<TextBlock
Margin="8,0,0,2"
VerticalAlignment="Center"
@@ -235,4 +235,4 @@
</StackPanel>
</ScrollViewer>
</Grid>
</shc:ScopedPage>
</shc:ScopedPage>

View File

@@ -13,7 +13,6 @@
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"
@@ -347,20 +346,10 @@
</Grid>
<Grid Visibility="{Binding GamePathSelectedAndValid, Converter={StaticResource BoolToVisibilityRevertConverter}}">
<StackPanel
Margin="128,0"
MaxWidth="600"
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}"
@@ -375,7 +364,9 @@
HeaderIcon="{shcm:FontIcon Glyph=&#xE7FC;}"
IsClickEnabled="True">
<cwc:SettingsCard.Description>
<TextBlock Foreground="{ThemeResource SystemErrorTextColor}" Text="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"/>
<StackPanel>
<TextBlock Foreground="{ThemeResource SystemErrorTextColor}" Text="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"/>
</StackPanel>
</cwc:SettingsCard.Description>
</cwc:SettingsCard>
</StackPanel>

View File

@@ -23,10 +23,7 @@
<ScrollViewer shch:ScrollViewerHelper.LeftPanelMaxWidth="800" Style="{StaticResource TwoPanelScrollViewerStyle}">
<shch:ScrollViewerHelper.RightPanel>
<StackPanel
Width="360"
Margin="0,16,16,16"
Spacing="{StaticResource SettingsCardSpacing}">
<StackPanel Width="360" Margin="0,16,16,16">
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Grid Style="{ThemeResource GridCardStyle}">
<Border
@@ -54,6 +51,16 @@
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"
@@ -86,27 +93,6 @@
</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">
@@ -160,17 +146,17 @@
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperHeader}"
Visibility="{Binding UserOptions.IsLicensedDeveloper, Converter={StaticResource BoolToVisibilityConverter}}">
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}}">
<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}"
@@ -205,6 +191,15 @@
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}"
@@ -234,47 +229,35 @@
Description="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE92E;}">
<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">
<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>
<ComboBox
MinWidth="120"
VerticalAlignment="Center"
@@ -322,19 +305,22 @@
</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,17 +78,15 @@
</cwc:SettingsCard>
<cwc:SettingsCard Header="Reset Guide State">
<Button
Command="{Binding ResetGuideStateCommand}"
Content="Reset (No restart)"
Style="{ThemeResource SettingButtonStyle}"/>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding ResetGuideStateCommand}" Content="Reset (No restart)"/>
</StackPanel>
</cwc:SettingsCard>
<cwc:SettingsCard Header="Resize MainWindow">
<Button
Command="{Binding ResetMainWindowSizeCommand}"
Content="Reset"
Style="{ThemeResource SettingButtonStyle}"/>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding ResetMainWindowSizeCommand}" Content="Reset"/>
</StackPanel>
</cwc:SettingsCard>
<cwc:SettingsCard Header="Suppress Metadata Initialization">
@@ -99,10 +97,6 @@
<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"

View File

@@ -14,8 +14,7 @@
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shcp="using:Snap.Hutao.Control.Panel"
xmlns:shct="using:Snap.Hutao.Control.Text"
xmlns:shvcont="using:Snap.Hutao.View.Control"
xmlns:shvconv="using:Snap.Hutao.View.Converter"
xmlns:shvc="using:Snap.Hutao.View.Control"
xmlns:shvcp="using:Snap.Hutao.View.Card.Primitive"
xmlns:shvw="using:Snap.Hutao.ViewModel.Wiki"
d:DataContext="{d:DesignInstance Type=shvw:WikiAvatarViewModel}"
@@ -25,29 +24,20 @@
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>
<shvconv:VisibilityToObjectConverter
x:Key="VisibilityToColumnSpanConverter"
CollapsedValue="{shcm:Int32 Value=2}"
VisibleValue="{shcm:Int32 Value=1}"/>
<DataTemplate x:Key="SkillDataTemplate">
<Grid Margin="0,16,0,0" ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<shct:DescriptionTextBlock
Grid.ColumnSpan="{Binding ElementName=ProudSelector, Path=Visibility, Converter={StaticResource VisibilityToColumnSpanConverter}}"
VerticalAlignment="Top"
Description="{Binding Description}">
<shct:DescriptionTextBlock VerticalAlignment="Top" Description="{Binding Description}">
<shct:DescriptionTextBlock.Resources>
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</shct:DescriptionTextBlock.Resources>
</shct:DescriptionTextBlock>
<shvcont:DescParamComboBox
x:Name="ProudSelector"
<shvc:DescParamComboBox
Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
@@ -57,7 +47,7 @@
</DataTemplate>
<DataTemplate x:Key="PropertyDataTemplate">
<shvcont:DescParamComboBox
<shvc:DescParamComboBox
HorizontalAlignment="Stretch"
PreferredSelectedIndex="13"
Source="{Binding Converter={StaticResource PropertyDescriptor}}"/>
@@ -96,7 +86,7 @@
<DataTemplate x:Key="CultivationItemTemplate">
<shvcp:HorizontalCard>
<shvcp:HorizontalCard.Left>
<shvcont:ItemIcon
<shvc:ItemIcon
Width="40"
Height="40"
Icon="{Binding Icon, Converter={StaticResource ItemIconConverter}}"
@@ -114,7 +104,7 @@
<DataTemplate x:Key="CollocationTemplate">
<shvcp:HorizontalCard>
<shvcp:HorizontalCard.Left>
<shvcont:ItemIcon
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{Binding Icon}"
@@ -141,7 +131,7 @@
<x:Int32>0</x:Int32>
</cwc:Case.Value>
<Grid>
<shvcont:ItemIcon
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{StaticResource UI_ItemIcon_None}"
@@ -153,7 +143,7 @@
<x:Int32>1</x:Int32>
</cwc:Case.Value>
<Grid>
<shvcont:ItemIcon
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{Binding Icons[0]}"
@@ -165,7 +155,7 @@
<x:Int32>2</x:Int32>
</cwc:Case.Value>
<Grid>
<shvcont:ItemIcon
<shvc:ItemIcon
Width="48"
Height="48"
Quality="QUALITY_ORANGE"/>
@@ -235,9 +225,9 @@
</DataTemplate>
<DataTemplate x:Key="AvatarGridTemplate">
<shvcont:BottomTextControl Text="{Binding Name}">
<shvcont:ItemIcon Icon="{Binding Icon, Converter={StaticResource AvatarIconConverter}, Mode=OneWay}" Quality="{Binding Quality}"/>
</shvcont:BottomTextControl>
<shvc:BottomTextControl Text="{Binding Name}">
<shvc:ItemIcon Icon="{Binding Icon, Converter={StaticResource AvatarIconConverter}, Mode=OneWay}" Quality="{Binding Quality}"/>
</shvc:BottomTextControl>
</DataTemplate>
</Page.Resources>
@@ -349,7 +339,7 @@
Height="32"
Source="{Binding Selected.Weapon, Converter={StaticResource WeaponTypeIconConverter}}"/>
</Grid>
<shvcont:ItemIcon
<shvc:ItemIcon
Width="128"
Height="128"
Icon="{Binding Selected.Icon, Converter={StaticResource AvatarIconConverter}, Mode=OneWay}"
@@ -473,7 +463,7 @@
<SolidColorBrush x:Key="SettingsCardBackgroundPointerOver" Color="Transparent"/>
<SolidColorBrush x:Key="SettingsCardBackgroundPressed" Color="Transparent"/>
</Border.Resources>
<shvcont:BaseValueSlider
<shvc:BaseValueSlider
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
BaseValueInfo="{Binding BaseValueInfo, Mode=OneWay}"/>
@@ -486,7 +476,7 @@
Padding="16"
Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}"
CornerRadius="{ThemeResource ControlCornerRadius}">
<shvcont:SkillPivot ItemTemplate="{StaticResource SkillDataTemplate}" Skills="{Binding Selected.SkillDepot.CompositeSkills}"/>
<shvc:SkillPivot ItemTemplate="{StaticResource SkillDataTemplate}" Skills="{Binding Selected.SkillDepot.CompositeSkills}"/>
</Border>
</Border>
@@ -496,7 +486,7 @@
Padding="16"
Background="{ThemeResource SystemControlAcrylicElementMediumHighBrush}"
CornerRadius="{ThemeResource ControlCornerRadius}">
<shvcont:SkillPivot ItemTemplate="{StaticResource TalentDataTemplate}" Skills="{Binding Selected.SkillDepot.Talents}"/>
<shvc:SkillPivot ItemTemplate="{StaticResource TalentDataTemplate}" Skills="{Binding Selected.SkillDepot.Talents}"/>
</Border>
</Border>
@@ -604,25 +594,25 @@
Grid.Column="0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarSpecialFoodTitle}"/>
<shvcont:BottomTextControl
<shvc:BottomTextControl
Grid.Row="1"
Grid.Column="0"
Margin="0,16,0,0"
Text="{Binding Item.Name}">
<shvcont:ItemIcon Icon="{Binding Item.Icon, Converter={StaticResource ItemIconConverter}}" Quality="{Binding Item.RankLevel}"/>
</shvcont:BottomTextControl>
<shvc:ItemIcon Icon="{Binding Item.Icon, Converter={StaticResource ItemIconConverter}}" Quality="{Binding Item.RankLevel}"/>
</shvc:BottomTextControl>
<TextBlock
Grid.Column="1"
Margin="16,0,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageWiKiAvatarOriginalFoodTitle}"/>
<shvcont:BottomTextControl
<shvc:BottomTextControl
Grid.Row="1"
Grid.Column="1"
Margin="16,16,0,0"
Text="{Binding OriginItem.Name}">
<shvcont:ItemIcon Icon="{Binding OriginItem.Icon, Converter={StaticResource ItemIconConverter}}" Quality="{Binding OriginItem.RankLevel}"/>
</shvcont:BottomTextControl>
<shvc:ItemIcon Icon="{Binding OriginItem.Icon, Converter={StaticResource ItemIconConverter}}" Quality="{Binding OriginItem.RankLevel}"/>
</shvc:BottomTextControl>
<StackPanel
Grid.RowSpan="4"
Grid.Column="2"
@@ -726,6 +716,6 @@
</cwc:Case>
</cwc:SwitchPresenter>
</Grid>
<shvcont:LoadingView IsLoading="{Binding Avatars, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
<shvc:LoadingView IsLoading="{Binding Avatars, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
</Grid>
</shc:ScopedPage>

View File

@@ -4,19 +4,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shv="using:Snap.Hutao.ViewModel"
Height="44"
VerticalAlignment="Top"
d:DataContext="{d:DesignInstance shv:TitleViewModel}"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Grid x:Name="DragableGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
@@ -28,70 +19,36 @@
Margin="4,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Title}"
Text="{x:Bind Title}"
TextWrapping="NoWrap"/>
<StackPanel
Grid.Column="1"
Margin="0,0,6,0"
Orientation="Horizontal"
Spacing="6">
<StackPanel
Orientation="Horizontal"
Spacing="6"
Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityConverter}}">
<ToggleButton
VerticalAlignment="Center"
IsChecked="{Binding HotKeyOptions.IsMouseClickRepeatForeverOn, Mode=OneWay}"
IsHitTestVisible="False"
Visibility="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.IsEnabled, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{shcm:ResourceString Name=ViewTitleAutoClicking}"/>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.DisplayName, Mode=OneWay}"/>
</Grid>
</ToggleButton>
</StackPanel>
<Grid
Margin="0,6"
Padding="12,0"
ColumnSpacing="12"
Style="{ThemeResource GridCardStyle}"
Visibility="{Binding UpdateStatus, Converter={StaticResource EmptyObjectToVisibilityConverter}, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ProgressBar
Grid.ColumnSpan="2"
MinHeight="32"
Margin="-12,0"
HorizontalAlignment="Stretch"
Background="Transparent"
CornerRadius="{ThemeResource ControlCornerRadius}"
Maximum="{Binding UpdateStatus.TotalBytes, Mode=OneWay}"
Opacity="{ThemeResource LargeBackgroundProgressBarOpacity}"
Value="{Binding UpdateStatus.BytesRead, Mode=OneWay}"/>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding UpdateStatus.ProgressDescription, Mode=OneWay}"/>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding UpdateStatus.VersionDescription, Mode=OneWay}"/>
</Grid>
Spacing="6"
Visibility="{x:Bind RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityConverter}}">
<ToggleButton
Margin="0,0,6,0"
VerticalAlignment="Center"
IsChecked="{x:Bind HotKeyOptions.IsMouseClickRepeatForeverOn, Mode=OneWay}"
IsHitTestVisible="False"
Visibility="{x:Bind HotKeyOptions.MouseClickRepeatForeverKeyCombination.IsEnabled, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{shcm:ResourceString Name=ViewTitleAutoClicking}"/>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind HotKeyOptions.MouseClickRepeatForeverKeyCombination.DisplayName, Mode=OneWay}"/>
</Grid>
</ToggleButton>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,10 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.ViewModel;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Windowing.HotKey;
namespace Snap.Hutao.View;
@@ -14,14 +14,39 @@ namespace Snap.Hutao.View;
[HighQuality]
internal sealed partial class TitleView : UserControl
{
/// <summary>
/// 构造一个新的标题视图
/// </summary>
public TitleView()
{
DataContext = Ioc.Default.GetRequiredService<TitleViewModel>();
InitializeComponent();
}
/// <summary>
/// 标题
/// </summary>
public string Title
{
[SuppressMessage("", "IDE0027")]
get
{
#if DEBUG
return SH.FormatAppDevNameAndVersion(RuntimeOptions.Version);
#else
return SH.FormatAppNameAndVersion(RuntimeOptions.Version);
#endif
}
}
/// <summary>
/// 获取可拖动区域
/// </summary>
public FrameworkElement DragArea
{
get => DragableGrid;
}
}
public RuntimeOptions RuntimeOptions { get; } = Ioc.Default.GetRequiredService<RuntimeOptions>();
public HotKeyOptions HotKeyOptions { get; } = Ioc.Default.GetRequiredService<HotKeyOptions>();
}

View File

@@ -23,7 +23,6 @@ using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using Windows.Win32.Foundation;
namespace Snap.Hutao.ViewModel.Game;
@@ -43,7 +42,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private readonly IContentDialogFactory contentDialogFactory;
private readonly LaunchStatusOptions launchStatusOptions;
private readonly IGameLocatorFactory gameLocatorFactory;
private readonly ILogger<LaunchGameViewModel> logger;
private readonly IProgressFactory progressFactory;
private readonly IInfoBarService infoBarService;
private readonly ResourceClient resourceClient;
@@ -263,7 +261,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{
await taskContext.SwitchToMainThreadAsync();
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
UpdateSelectedGamePathEntry(entry, true);
UpdateSelectedGamePathEntry(entry, false);
}
}
@@ -281,13 +279,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
}
catch (Exception ex)
{
if (ex is Win32Exception win32Exception && win32Exception.HResult == HRESULT.E_FAIL)
{
// User canceled the operation. ignore
return;
}
logger.LogCritical(ex, "Launch failed");
infoBarService.Error(ex);
}
}

View File

@@ -6,7 +6,6 @@ using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Notification;
using System.Collections.ObjectModel;
using Windows.Win32.Foundation;
namespace Snap.Hutao.ViewModel.Game;
@@ -72,12 +71,6 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
}
catch (Exception ex)
{
if (ex is Win32Exception win32Exception && win32Exception.HResult == HRESULT.E_FAIL)
{
// User canceled the operation. ignore
return;
}
infoBarService.Error(ex);
}
}

View File

@@ -16,7 +16,6 @@ internal static class StaticResource
private static readonly ApplicationDataCompositeValue DefaultResourceVersionMap = new()
{
// DO NOT MIDIFY THIS MAP
{ "AchievementIcon", 0 },
{ "AvatarCard", 0 },
{ "AvatarIcon", 0 },
@@ -48,29 +47,29 @@ internal static class StaticResource
{
{ "AchievementIcon", 1 },
{ "AvatarCard", 1 },
{ "AvatarIcon", 4 },
{ "AvatarIcon", 3 },
{ "Bg", 2 },
{ "ChapterIcon", 2 },
{ "ChapterIcon", 1 },
{ "CodexMonster", 0 },
{ "Costume", 1 },
{ "EmotionIcon", 2 },
{ "EquipIcon", 3 },
{ "GachaAvatarIcon", 3 },
{ "GachaAvatarImg", 3 },
{ "GachaEquipIcon", 3 },
{ "EmotionIcon", 1 },
{ "EquipIcon", 2 },
{ "GachaAvatarIcon", 2 },
{ "GachaAvatarImg", 2 },
{ "GachaEquipIcon", 2 },
{ "GcgCharAvatarIcon", 0 },
{ "IconElement", 2 },
{ "ItemIcon", 3 },
{ "ItemIcon", 2 },
{ "LoadingPic", 1 },
{ "MonsterIcon", 2 },
{ "MonsterSmallIcon", 1 },
{ "NameCardIcon", 2 },
{ "NameCardPic", 3 },
{ "NameCardIcon", 1 },
{ "NameCardPic", 2 },
{ "NameCardPicAlpha", 0 },
{ "Property", 1 },
{ "RelicIcon", 3 },
{ "Skill", 3 },
{ "Talent", 3 },
{ "RelicIcon", 2 },
{ "Skill", 2 },
{ "Talent", 2 },
};
public static void FulfillAll()

View File

@@ -2,10 +2,8 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.View.Card;
using Snap.Hutao.View.Card.Primitive;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
@@ -25,8 +23,6 @@ internal sealed partial class AnnouncementViewModel : Abstraction.ViewModel
private readonly IAnnouncementService announcementService;
private readonly HutaoUserOptions hutaoUserOptions;
private readonly ITaskContext taskContext;
private readonly MetadataOptions metadataOptions;
private readonly AppOptions appOptions;
private AnnouncementWrapper? announcement;
private string greetingText = SH.ViewPageHomeGreetingTextDefault;
@@ -65,7 +61,7 @@ internal sealed partial class AnnouncementViewModel : Abstraction.ViewModel
{
try
{
AnnouncementWrapper announcementWrapper = await announcementService.GetAnnouncementWrapperAsync(metadataOptions.LanguageCode, appOptions.Region, CancellationToken).ConfigureAwait(false);
AnnouncementWrapper announcementWrapper = await announcementService.GetAnnouncementWrapperAsync(CancellationToken).ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
Announcement = announcementWrapper;
}
@@ -142,4 +138,4 @@ internal sealed partial class AnnouncementViewModel : Abstraction.ViewModel
Cards = result;
}
}
}

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Shell;
@@ -22,10 +23,8 @@ using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.ViewModel.Guide;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Response;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
@@ -62,7 +61,6 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
private NameValue<BackdropType>? selectedBackdropType;
private NameValue<CultureInfo>? selectedCulture;
private NameValue<Region>? selectedRegion;
private IPInformation? ipInformation;
private FolderViewModel? cacheFolderView;
private FolderViewModel? dataFolderView;
@@ -106,18 +104,6 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
}
}
public NameValue<Region>? SelectedRegion
{
get => selectedRegion ??= AppOptions.GetCurrentRegionForSelectionOrDefault();
set
{
if (SetProperty(ref selectedRegion, value) && value is not null)
{
AppOptions.Region = value.Value;
}
}
}
public FolderViewModel? CacheFolderView { get => cacheFolderView; set => SetProperty(ref cacheFolderView, value); }
public FolderViewModel? DataFolderView { get => dataFolderView; set => SetProperty(ref dataFolderView, value); }
@@ -174,6 +160,18 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
await Launcher.LaunchUriAsync(new("ms-windows-store://pdp/?productid=9PH4NXJ2JN52"));
}
[Command("SetPowerShellPathCommand")]
private async Task SetPowerShellPathAsync()
{
(bool isOk, ValueFile file) = fileSystemPickerInteraction.PickFile(SH.FilePickerPowerShellCommit, [("PowerShell", "powershell.exe;pwsh.exe")]);
if (isOk && Path.GetFileNameWithoutExtension(file).EqualsAny(["POWERSHELL", "PWSH"], StringComparison.OrdinalIgnoreCase))
{
await taskContext.SwitchToMainThreadAsync();
AppOptions.PowerShellPath = file;
}
}
[Command("DeleteGameWebCacheCommand")]
private void DeleteGameWebCache()
{
@@ -282,17 +280,4 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
infoBarService.Warning(SH.ViewModelSettingCreateDesktopShortcutFailed);
}
}
[Command("RestartAsElevatedCommand")]
private void RestartAsElevated()
{
Process.Start(new ProcessStartInfo()
{
FileName = $"shell:AppsFolder\\{runtimeOptions.FamilyName}!App",
UseShellExecute = true,
Verb = "runas",
});
Process.GetCurrentProcess().Kill();
}
}

View File

@@ -39,13 +39,6 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel
set => LocalSetting.Set(SettingKeys.OverrideElevationRequirement, value);
}
[SuppressMessage("", "CA1822")]
public bool OverrideUpdateVersionComparison
{
get => LocalSetting.Get(SettingKeys.OverrideUpdateVersionComparison, false);
set => LocalSetting.Set(SettingKeys.OverrideUpdateVersionComparison, value);
}
[Command("ResetGuideStateCommand")]
private static void ResetGuideState()
{
@@ -62,7 +55,7 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel
private void ResetMainWindowSize()
{
double scale = mainWindow.WindowOptions.GetRasterizationScale();
mainWindow.AppWindow.Resize(new Windows.Graphics.SizeInt32(1372, 772).Scale(scale));
mainWindow.AppWindow.Resize(new Windows.Graphics.SizeInt32(1280, 720).Scale(scale));
}
[Command("UploadAnnouncementCommand")]

View File

@@ -1,81 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Windowing.HotKey;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Update;
using System.Globalization;
using System.Text;
namespace Snap.Hutao.ViewModel;
[ConstructorGenerated]
[Injection(InjectAs.Singleton)]
internal sealed partial class TitleViewModel : Abstraction.ViewModel
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IProgressFactory progressFactory;
private readonly RuntimeOptions runtimeOptions;
private readonly HotKeyOptions hotKeyOptions;
private readonly IUpdateService updateService;
private readonly ITaskContext taskContext;
private UpdateStatus? updateStatus;
public RuntimeOptions RuntimeOptions { get => runtimeOptions; }
public HotKeyOptions HotKeyOptions { get => hotKeyOptions; }
public string Title
{
[SuppressMessage("", "IDE0027")]
get
{
string name = new StringBuilder()
.Append("App")
.AppendIf(runtimeOptions.IsElevated, "Elevated")
#if DEBUG
.Append("Dev")
#endif
.Append("NameAndVersion")
.ToString();
string? format = SH.GetString(CultureInfo.CurrentCulture, name);
ArgumentException.ThrowIfNullOrEmpty(format);
return string.Format(CultureInfo.CurrentCulture, format, runtimeOptions.Version);
}
}
public UpdateStatus? UpdateStatus { get => updateStatus; set => SetProperty(ref updateStatus, value); }
protected override async ValueTask<bool> InitializeUIAsync()
{
await DoCheckUpdateAsync().ConfigureAwait(false);
return true;
}
private async ValueTask DoCheckUpdateAsync()
{
IProgress<UpdateStatus> progress = progressFactory.CreateForMainThread<UpdateStatus>(status => UpdateStatus = status);
if (await updateService.CheckForUpdateAndDownloadAsync(progress).ConfigureAwait(false))
{
ContentDialogResult result = await contentDialogFactory
.CreateForConfirmCancelAsync(
SH.FormatViewTitileUpdatePackageReadyTitle(UpdateStatus?.Version),
SH.ViewTitileUpdatePackageReadyContent,
ContentDialogButton.Primary)
.ConfigureAwait(false);
if (result == ContentDialogResult.Primary)
{
await updateService.LaunchUpdaterAsync().ConfigureAwait(false);
}
}
await taskContext.SwitchToMainThreadAsync();
UpdateStatus = null;
}
}

View File

@@ -281,24 +281,12 @@ internal static class ApiEndpoints
/// <summary>
/// 公告列表
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <param name="region">服务器</param>
/// <returns>公告列表Url</returns>
public static string AnnList(string languageCode, in Region region)
{
return $"{Hk4eApiAnnouncementApi}/getAnnList?{AnnouncementQuery(languageCode, region)}";
}
public const string AnnList = $"{Hk4eApiAnnouncementApi}/getAnnList?{AnnouncementQuery}";
/// <summary>
/// 公告内容
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <param name="region">服务器</param>
/// <returns>公告列表Url</returns>
public static string AnnContent(string languageCode, in Region region)
{
return $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery(languageCode, region)}";
}
public const string AnnContent = $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery}";
#endregion
#region Hk4eSdk
@@ -434,9 +422,6 @@ internal static class ApiEndpoints
/// </summary>
public const string WebStaticMihoyoReferer = "https://webstatic.mihoyo.com";
private static string AnnouncementQuery(string languageCode, in Region region)
{
return $"game=hk4e&game_biz=hk4e_cn&lang={languageCode}&bundle_id=hk4e_cn&platform=pc&region={region}&level=55&uid=100000000";
}
private const string AnnouncementQuery = "game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc&region=cn_gf01&level=55&uid=100000000";
#endregion
}

View File

@@ -100,7 +100,7 @@ internal static class ApiOsEndpoints
/// </summary>
/// <param name="region">地区代号</param>
/// <returns>用户游戏角色字符串</returns>
public static string UserGameRolesByLtoken(in Region region)
public static string UserGameRolesByLtoken(string region)
{
return $"{ApiAccountOsBindingApi}/getUserGameRolesByLtoken?game_biz=hk4e_global&region={region}";
}
@@ -189,32 +189,6 @@ internal static class ApiOsEndpoints
}
#endregion
#region Hk4eApiOsAnnouncementApi
/// <summary>
/// 公告列表
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <param name="region">服务器</param>
/// <returns>公告列表Url</returns>
public static string AnnList(string languageCode, in Region region)
{
return $"{Hk4eApiOsAnnouncementApi}/getAnnList?{AnnouncementQuery(languageCode, region)}";
}
/// <summary>
/// 公告内容
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <param name="region">服务器</param>
/// <returns>公告内容Url</returns>
public static string AnnContent(string languageCode, in Region region)
{
return $"{Hk4eApiOsAnnouncementApi}/getAnnContent?{AnnouncementQuery(languageCode, region)}";
}
#endregion
#region SgPublicApi
/// <summary>
@@ -333,7 +307,6 @@ internal static class ApiOsEndpoints
private const string BbsApiOsGameRecordAppApi = $"{BbsApiOs}/game_record/app/genshin/api";
private const string Hk4eApiOs = "https://hk4e-api-os.hoyoverse.com";
private const string Hk4eApiOsAnnouncementApi = $"{Hk4eApiOs}/common/hk4e_global/announcement/api";
private const string Hk4eApiOsGachaInfoApi = $"{Hk4eApiOs}/gacha_info/api";
private const string SdkOsStatic = "https://sdk-os-static.mihoyo.com";
@@ -360,10 +333,5 @@ internal static class ApiOsEndpoints
/// </summary>
public const string AppHoyolabReferer = "https://app.hoyolab.com/";
private static string AnnouncementQuery(string languageCode, in Region region)
{
return $"game=hk4e&game_biz=hk4e_global&lang={languageCode}&bundle_id=hk4e_global&platform=pc&region={region}&level=55&uid=100000000";
}
#endregion
}

View File

@@ -12,6 +12,7 @@ using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using Snap.Hutao.Web.Response;
using System.Runtime.InteropServices;
using System.Text;
using Windows.Foundation;
@@ -403,12 +404,20 @@ internal class MiHoYoJSBridge
logger?.LogInformation("[{Id}][ExecuteScript: {callback}]\n{payload}", interfaceId, callback, payload);
await taskContext.SwitchToMainThreadAsync();
if (coreWebView2 is null || coreWebView2.IsDisposed())
try
{
return string.Empty;
if (coreWebView2 is not null)
{
return await coreWebView2.ExecuteScriptAsync(js);
}
}
catch (COMException)
{
// COMException (0x8007139F): 组或资源的状态不是执行请求操作的正确状态。 (0x8007139F)
// webview is disposing or disposed
}
return await coreWebView2.ExecuteScriptAsync(js);
return string.Empty;
}
private async void OnWebMessageReceived(CoreWebView2 webView2, CoreWebView2WebMessageReceivedEventArgs args)

View File

@@ -7,4 +7,10 @@ namespace Snap.Hutao.Web.Bridge.Model;
/// 指示此为Js结果
/// </summary>
[HighQuality]
internal interface IJsBridgeResult;
internal interface IJsBridgeResult
{
string ToJson()
{
return JsonSerializer.Serialize(this);
}
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Bridge.Model;
internal static class JsBridgeResultExtension
{
public static string ToJson<T>(this T result)
where T : IJsBridgeResult
{
return JsonSerializer.Serialize(result, result.GetType());
}
}

View File

@@ -80,7 +80,7 @@ internal sealed class Announcement : AnnouncementContent
/// </summary>
public string TimeFormatted
{
get => $"{StartTime.ToLocalTime():yyyy.MM.dd HH:mm} - {EndTime.ToLocalTime():yyyy.MM.dd HH:mm}";
get => $"{StartTime:yyyy.MM.dd HH:mm} - {EndTime:yyyy.MM.dd HH:mm}";
}
#endregion

View File

@@ -23,18 +23,12 @@ internal sealed partial class AnnouncementClient
/// <summary>
/// 异步获取公告列表
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <param name="region">服务器</param>
/// <param name="token">取消令牌</param>
/// <returns>公告列表</returns>
public async ValueTask<Response<AnnouncementWrapper>> GetAnnouncementsAsync(string languageCode, Region region, CancellationToken token = default)
public async ValueTask<Response<AnnouncementWrapper>> GetAnnouncementsAsync(CancellationToken token = default)
{
string annListUrl = region.IsOversea()
? ApiOsEndpoints.AnnList(languageCode, region)
: ApiEndpoints.AnnList(languageCode, region);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(annListUrl)
.SetRequestUri(ApiEndpoints.AnnList)
.Get();
Response<AnnouncementWrapper>? resp = await builder
@@ -47,18 +41,12 @@ internal sealed partial class AnnouncementClient
/// <summary>
/// 异步获取公告内容列表
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <param name="region">服务器</param>
/// <param name="token">取消令牌</param>
/// <returns>公告内容列表</returns>
public async ValueTask<Response<ListWrapper<AnnouncementContent>>> GetAnnouncementContentsAsync(string languageCode, Region region, CancellationToken token = default)
public async ValueTask<Response<ListWrapper<AnnouncementContent>>> GetAnnouncementContentsAsync(CancellationToken token = default)
{
string annContentUrl = region.IsOversea()
? ApiOsEndpoints.AnnContent(languageCode, region)
: ApiEndpoints.AnnContent(languageCode, region);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(annContentUrl)
.SetRequestUri(ApiEndpoints.AnnContent)
.Get();
Response<ListWrapper<AnnouncementContent>>? resp = await builder

View File

@@ -7,23 +7,19 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
internal static partial class AnnouncementRegex
{
/// <inheritdoc cref="SH.WebAnnouncementMatchVersionUpdateTitle"/>
public static readonly Regex VersionUpdateTitleRegex = new(SH.WebAnnouncementMatchVersionUpdateTitle, RegexOptions.Compiled);
/// <inheritdoc cref="SH.WebAnnouncementMatchVersionUpdateTime"/>
public static readonly Regex VersionUpdateTimeRegex = new(SH.WebAnnouncementMatchVersionUpdateTime, RegexOptions.Compiled);
/// <inheritdoc cref="SH.WebAnnouncementMatchTransientActivityTime"/>
public static readonly Regex TransientActivityAfterUpdateTimeRegex = new(SH.WebAnnouncementMatchTransientActivityTime, RegexOptions.Compiled);
/// <inheritdoc cref="SH.WebAnnouncementMatchPersistentActivityTime"/>
public static readonly Regex PersistentActivityAfterUpdateTimeRegex = new(SH.WebAnnouncementMatchPersistentActivityTime, RegexOptions.Compiled);
/// <inheritdoc cref="SH.WebAnnouncementMatchPermanentActivityTime"/>
public static readonly Regex PermanentActivityAfterUpdateTimeRegex = new(SH.WebAnnouncementMatchPermanentActivityTime, RegexOptions.Compiled);
/// <inheritdoc cref="XmlTagRegex"/>
public static readonly Regex XmlTimeTagRegex = XmlTagRegex();
public static readonly Regex XmlTimeTagRegex = XmlTimeTagRegexInner ??= XmlTagRegex();
private static readonly Regex? XmlTimeTagRegexInner;
[GeneratedRegex("&lt;t class=\"t_(?:gl|lc)\".*?&gt;(.*?)&lt;/t&gt;", RegexOptions.Multiline)]
private static partial Regex XmlTagRegex();

View File

@@ -41,8 +41,7 @@ internal sealed class GachaLogPage : IJsonOnDeserialized
/// 地区
/// </summary>
[JsonPropertyName("region")]
[JsonConverter(typeof(RegionConverter))]
public Region Region { get; set; } = default!;
public string Region { get; set; } = default!;
public void OnDeserialized()
{

View File

@@ -1,15 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.RegularExpressions;
namespace Snap.Hutao.Web.Hoyolab;
internal static partial class HoyolabRegex
{
[GeneratedRegex("^[1-9][0-9]{8}$")]
public static partial Regex UidRegex();
[GeneratedRegex("^(cn_gf01|cn_qd01|os_usa|os_euro|os_asia|os_cht)$")]
public static partial Regex RegionRegex();
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.RegularExpressions;
namespace Snap.Hutao.Web.Hoyolab;
/// <summary>
@@ -17,18 +19,18 @@ internal readonly partial struct PlayerUid
/// <summary>
/// 地区代码
/// </summary>
public readonly Region Region;
public readonly string Region;
/// <summary>
/// 构造一个新的玩家 Uid 结构
/// </summary>
/// <param name="value">uid</param>
/// <param name="region">服务器,当提供该参数时会无条件信任</param>
public PlayerUid(string value, in Region? region = default)
public PlayerUid(string value, string? region = default)
{
Must.Argument(HoyolabRegex.UidRegex().IsMatch(value), SH.WebHoyolabInvalidUid);
Must.Argument(UidRegex().IsMatch(value), SH.WebHoyolabInvalidUid);
Value = value;
Region = region ?? Region.FromUidString(value);
Region = region ?? EvaluateRegion(value.AsSpan()[0]);
}
public static implicit operator PlayerUid(string source)
@@ -43,7 +45,7 @@ internal readonly partial struct PlayerUid
public static bool IsOversea(string uid)
{
Must.Argument(HoyolabRegex.UidRegex().IsMatch(uid), SH.WebHoyolabInvalidUid);
Must.Argument(UidRegex().IsMatch(uid), SH.WebHoyolabInvalidUid);
return uid.AsSpan()[0] switch
{
@@ -54,7 +56,7 @@ internal readonly partial struct PlayerUid
public static TimeSpan GetRegionTimeZoneUtcOffsetForUid(string uid)
{
Must.Argument(HoyolabRegex.UidRegex().IsMatch(uid), SH.WebHoyolabInvalidUid);
Must.Argument(UidRegex().IsMatch(uid), SH.WebHoyolabInvalidUid);
// 美服 UTC-05
// 欧服 UTC+01
@@ -67,12 +69,12 @@ internal readonly partial struct PlayerUid
};
}
public static TimeSpan GetRegionTimeZoneUtcOffsetForRegion(in Region region)
public static TimeSpan GetRegionTimeZoneUtcOffsetForRegion(string region)
{
// 美服 UTC-05
// 欧服 UTC+01
// 其他 UTC+08
return region.Value switch
return region switch
{
"os_usa" => ServerRegionTimeZone.AmericaServerOffset,
"os_euro" => ServerRegionTimeZone.EuropeServerOffset,
@@ -85,4 +87,24 @@ internal readonly partial struct PlayerUid
{
return Value;
}
private static string EvaluateRegion(in char first)
{
return first switch
{
// CN
>= '1' and <= '4' => "cn_gf01", // 国服
'5' => "cn_qd01", // 渠道
// OS
'6' => "os_usa", // 美服
'7' => "os_euro", // 欧服
'8' => "os_asia", // 亚服
'9' => "os_cht", // 台服
_ => throw Must.NeverHappen(),
};
}
[GeneratedRegex("^[1-9][0-9]{8}$")]
private static partial Regex UidRegex();
}

View File

@@ -12,7 +12,7 @@ internal static class PlayerUidExtension
{
NameValueCollection collection = [];
collection.Set("role_id", playerUid.Value);
collection.Set("server", playerUid.Region.Value);
collection.Set("server", playerUid.Region);
return collection.ToQueryString();
}
@@ -21,7 +21,7 @@ internal static class PlayerUidExtension
{
NameValueCollection collection = [];
collection.Set("uid", playerUid.Value);
collection.Set("region", playerUid.Region.Value);
collection.Set("region", playerUid.Region);
return collection.ToQueryString();
}

View File

@@ -1,70 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab;
[JsonConverter(typeof(RegionConverter))]
internal readonly partial struct Region
{
public static readonly Region CNGF01 = new("cn_gf01");
public static readonly Region CNQD01 = new("cn_qd01");
public static readonly Region OSUSA = new("os_usa");
public static readonly Region OSEURO = new("os_euro");
public static readonly Region OSASIA = new("os_asia");
public static readonly Region OSCHT = new("os_cht");
public readonly string Value;
public Region(string value)
{
Must.Argument(HoyolabRegex.RegionRegex().IsMatch(value), SH.WebHoyolabInvalidRegion);
Value = value;
}
public static implicit operator Region(string value)
{
return FromRegionString(value);
}
public static Region FromRegionString(string value)
{
return new(value);
}
public static Region FromUidString(string uid)
{
return uid.AsSpan()[0] switch
{
// CN
>= '1' and <= '4' => new("cn_gf01"), // 国服
'5' => new("cn_qd01"), // 渠道
// OS
'6' => new("os_usa"), // 美服
'7' => new("os_euro"), // 欧服
'8' => new("os_asia"), // 亚服
'9' => new("os_cht"), // 台服
_ => throw Must.NeverHappen(),
};
}
public static bool IsOversea(string value)
{
Must.Argument(HoyolabRegex.RegionRegex().IsMatch(value), SH.WebHoyolabInvalidRegion);
return value.AsSpan()[..2] switch
{
"os" => true,
_ => false,
};
}
public readonly bool IsOversea()
{
return IsOversea(Value);
}
public override string ToString()
{
return Value;
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab;
internal sealed class RegionConverter : JsonConverter<Region>
{
public override Region Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.GetString() is { } regionValue)
{
return Region.FromRegionString(regionValue);
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, Region value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.Value);
}
}

View File

@@ -50,7 +50,7 @@ internal sealed class GenAuthKeyData
/// 区域
/// </summary>
[JsonPropertyName("region")]
public Region Region { get; set; } = default!;
public string Region { get; set; } = default!;
/// <summary>
/// 创建为祈愿记录验证密钥提交数据

View File

@@ -19,7 +19,7 @@ internal sealed class UserGameRole
/// 服务器
/// </summary>
[JsonPropertyName("region")]
public Region Region { get; set; } = default!;
public string Region { get; set; } = default!;
/// <summary>
/// 游戏Uid

View File

@@ -31,7 +31,7 @@ internal sealed class SignInData
/// 地区代码
/// </summary>
[JsonPropertyName("region")]
public Region Region { get; }
public string Region { get; }
/// <summary>
/// Uid

View File

@@ -181,7 +181,7 @@ internal sealed partial class CalculateClient
public string Uid { get; set; } = default!;
[JsonPropertyName("region")]
public Region Region { get; set; } = default!;
public string Region { get; set; } = default!;
}
private class IdCount

View File

@@ -39,5 +39,5 @@ internal sealed class CharacterData
/// 服务器
/// </summary>
[JsonPropertyName("server")]
public Region Server { get; }
public string Server { get; }
}

View File

@@ -22,10 +22,10 @@ internal sealed class Role
public string Nickname { get; set; } = default!;
/// <summary>
/// 服务器名称
/// 服务器
/// </summary>
[JsonPropertyName("region")]
public string RegionName { get; set; } = default!;
public string Region { get; set; } = default!;
/// <summary>
/// 等级

View File

@@ -23,10 +23,10 @@ internal sealed class BasicRoleInfo
public string Nickname { get; set; } = default!;
/// <summary>
/// 服务器名称
/// 区域代码
/// </summary>
[JsonPropertyName("region")]
public string RegionName { get; set; } = default!;
public string Region { get; set; } = default!;
/// <summary>
/// 等级

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
internal sealed class ArchonQuest
{
[JsonPropertyName("status")]
public ArchonQuestStatus Status { get; set; }
/// <summary>
/// 第X章 第Y幕
/// </summary>
[JsonPropertyName("chapter_num")]
public string ChapterNum { get; set; } = default!;
[JsonPropertyName("chapter_title")]
public string ChapterTitle { get; set; } = default!;
[JsonPropertyName("id")]
public uint Id { get; set; }
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
internal sealed class ArchonQuestProgress
{
[JsonPropertyName("list")]
public List<ArchonQuest> List { get; set; } = default!;
[JsonPropertyName("is_open_archon_quest")]
public bool IsOpenArchonQuest { get; set; }
[JsonPropertyName("is_finish_all_mainline")]
public bool IsFinishAllMainline { get; set; }
[JsonPropertyName("is_finish_all_interchapter")]
public bool IsFinishAllInterchapter { get; set; }
[JsonPropertyName("wiki_url")]
public string WikiUrl { get; set; } = default!;
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
[Localization]
internal enum ArchonQuestStatus
{
[LocalizationKey("WebDailyNoteArchonQuestStatusFinished")]
StatusFinished,
[LocalizationKey("WebDailyNoteArchonQuestStatusOngoing")]
StatusOngoing,
[LocalizationKey("WebDailyNoteArchonQuestStatusNotOpen")]
StatusNotOpen,
}

View File

@@ -7,16 +7,20 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
/// 实时便笺
/// </summary>
[HighQuality]
[SuppressMessage("", "SA1124")]
internal sealed class DailyNote : DailyNoteCommon
{
#region Binding
/// <summary>
/// 格式化的树脂显示
/// </summary>
[JsonIgnore]
public string ResinFormatted
{
get => $"{CurrentResin}/{MaxResin}";
}
/// <summary>
/// 格式化的树脂恢复时间
/// </summary>
[JsonIgnore]
public string ResinRecoveryTargetTime
{
@@ -41,12 +45,18 @@ internal sealed class DailyNote : DailyNoteCommon
}
}
/// <summary>
/// 格式化任务
/// </summary>
[JsonIgnore]
public string TaskFormatted
{
get => $"{FinishedTaskNum}/{TotalTaskNum}";
}
/// <summary>
/// 每日委托奖励字符串
/// </summary>
[JsonIgnore]
public string ExtraTaskRewardDescription
{
@@ -60,24 +70,53 @@ internal sealed class DailyNote : DailyNoteCommon
}
}
/// <summary>
/// 剩余周本折扣次数
/// </summary>
[JsonPropertyName("remain_resin_discount_num")]
public int RemainResinDiscountNum { get; set; }
/// <summary>
/// 周本树脂减免使用次数
/// </summary>
[JsonIgnore]
public int ResinDiscountUsedNum
{
get => ResinDiscountNumLimit - RemainResinDiscountNum;
}
[JsonIgnore]
/// <summary>
/// 周本折扣总次数
/// </summary>
[JsonPropertyName("resin_discount_num_limit")]
public int ResinDiscountNumLimit { get; set; }
/// <summary>
/// 格式化周本
/// </summary>
public string ResinDiscountFormatted
{
get => $"{ResinDiscountUsedNum}/{ResinDiscountNumLimit}";
}
/// <summary>
/// 洞天宝钱恢复时间 <see cref="string"/>类型的秒数
/// </summary>
[JsonPropertyName("home_coin_recovery_time")]
public int HomeCoinRecoveryTime { get; set; }
/// <summary>
/// 格式化洞天宝钱
/// </summary>
[JsonIgnore]
public string HomeCoinFormatted
{
get => MaxHomeCoin == 0 ? SH.WebDailyNoteHomeLocked : $"{CurrentHomeCoin}/{MaxHomeCoin}";
}
/// <summary>
/// 格式化的洞天宝钱恢复时间
/// </summary>
[JsonIgnore]
public string HomeCoinRecoveryTargetTimeFormatted
{
@@ -95,25 +134,6 @@ internal sealed class DailyNote : DailyNoteCommon
return SH.FormatWebDailyNoteHomeCoinRecoveryFormat(day, reach);
}
}
#endregion
/// <summary>
/// 剩余周本折扣次数
/// </summary>
[JsonPropertyName("remain_resin_discount_num")]
public int RemainResinDiscountNum { get; set; }
/// <summary>
/// 周本折扣总次数
/// </summary>
[JsonPropertyName("resin_discount_num_limit")]
public int ResinDiscountNumLimit { get; set; }
/// <summary>
/// 洞天宝钱恢复时间 <see cref="string"/>类型的秒数
/// </summary>
[JsonPropertyName("home_coin_recovery_time")]
public int HomeCoinRecoveryTime { get; set; }
/// <summary>
/// 日历链接
@@ -129,7 +149,4 @@ internal sealed class DailyNote : DailyNoteCommon
[JsonPropertyName("daily_task")]
public DailyTask DailyTask { get; set; } = default!;
[JsonPropertyName("archon_quest_progress")]
public ArchonQuestProgress ArchonQuestProgress { get; set; } = default!;
}

View File

@@ -7,10 +7,4 @@ internal sealed class HutaoVersionInformation
{
[JsonPropertyName("version")]
public Version Version { get; set; } = default!;
[JsonPropertyName("urls")]
public List<string> Urls { get; set; } = default!;
[JsonPropertyName("sha256")]
public string? Sha256 { get; set; } = default!;
}

View File

@@ -1,11 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.RegularExpressions;
namespace Snap.Hutao.Web.Hutao;
internal sealed partial class IPInformation
internal sealed class IPInformation
{
private const string Unknown = "Unknown";
@@ -28,10 +26,6 @@ internal sealed partial class IPInformation
return SH.WebHutaoServiceUnAvailable;
}
string maskedIp = IpRegex().Replace(Ip, "$1.$2.*.*");
return SH.FormatViewPageSettingDeviceIpDescription(maskedIp, Division);
return SH.FormatViewPageSettingDeviceIpDescription(Ip, Division);
}
[GeneratedRegex(@"(\d+)\.(\d+)\.\d+\.\d+")]
private static partial Regex IpRegex();
}