Compare commits

..

2 Commits

Author SHA1 Message Date
DismissedLight
98c003ae77 Merge branch 'develop' into feat/1239 2024-01-02 13:18:32 +08:00
qhy040404
d26611ccf7 impl #1239 2024-01-01 20:13:11 +08:00
15 changed files with 207 additions and 363 deletions

View File

@@ -11,12 +11,6 @@ namespace Snap.Hutao.Test.IncomingFeature;
[TestClass]
public class GameRegistryContentTest
{
private static readonly JsonSerializerOptions RegistryContentSerializerOptions = new()
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
[TestMethod]
[SupportedOSPlatform("windows")]
public void GetRegistryContent()
@@ -39,29 +33,28 @@ public class GameRegistryContentTest
data[valueName] = gameKey.GetValueKind(valueName) switch
{
RegistryValueKind.DWord => (int)gameKey.GetValue(valueName)!,
RegistryValueKind.Binary => GetStringOrObject((byte[])gameKey.GetValue(valueName)!),
RegistryValueKind.Binary => GetString((byte[])gameKey.GetValue(valueName)!),
_ => throw new NotImplementedException()
};
}
JsonSerializerOptions options = new()
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
Console.WriteLine($"Subkey: {subkey}");
Console.WriteLine(JsonSerializer.Serialize(data, RegistryContentSerializerOptions));
Console.WriteLine(JsonSerializer.Serialize(data, options));
}
}
private static unsafe object GetStringOrObject(byte[] bytes)
private static unsafe string GetString(byte[] bytes)
{
fixed (byte* pByte = bytes)
{
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
string temp = Encoding.UTF8.GetString(span);
if (temp.AsSpan()[0] is '{' or '[')
{
return JsonSerializer.Deserialize<JsonElement>(temp);
}
return temp;
return Encoding.UTF8.GetString(span);
}
}
}

View File

@@ -4,6 +4,8 @@
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Web;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Image;

View File

@@ -1,18 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Threading;
internal delegate bool SpinWaitPredicate<T>(ref readonly T state);
internal static class SpinWaitPolyfill
{
public static unsafe void SpinUntil<T>(ref T state, delegate*<ref readonly T, bool> condition)
{
SpinWait spinner = default;
while (!condition(ref state))
{
spinner.SpinOnce();
}
}
}

View File

@@ -8,45 +8,123 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
internal sealed partial class SettingEntry
{
/// <summary>
/// 游戏路径
/// </summary>
public const string GamePath = "GamePath";
public const string GamePathEntries = "GamePathEntries";
public const string Culture = "Culture";
public const string SystemBackdropType = "SystemBackdropType";
public const string AnnouncementRegion = "AnnouncementRegion";
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
public const string GeetestCustomCompositeUrl = "GeetestCustomCompositeUrl";
public const string DailyNoteRefreshSeconds = "DailyNote.RefreshSeconds";
public const string DailyNoteReminderNotify = "DailyNote.ReminderNotify";
public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame";
public const string DailyNoteWebhookUrl = "DailyNote.WebhookUrl";
public const string IsAdvancedLaunchOptionsEnabled = "IsAdvancedLaunchOptionsEnabled";
public const string LaunchIsLaunchOptionsEnabled = "Launch.IsLaunchOptionsEnabled";
public const string LaunchIsExclusive = "Launch.IsExclusive";
public const string LaunchIsFullScreen = "Launch.IsFullScreen";
public const string LaunchIsBorderless = "Launch.IsBorderless";
public const string LaunchScreenWidth = "Launch.ScreenWidth";
public const string LaunchIsScreenWidthEnabled = "Launch.IsScreenWidthEnabled";
public const string LaunchScreenHeight = "Launch.ScreenHeight";
public const string LaunchIsScreenHeightEnabled = "Launch.IsScreenHeightEnabled";
public const string LaunchUnlockFps = "Launch.UnlockFps";
public const string LaunchTargetFps = "Launch.TargetFps";
public const string LaunchMonitor = "Launch.Monitor";
public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled";
public const string LaunchIsUseCloudThirdPartyMobile = "Launch.IsUseCloudThirdPartyMobile";
public const string LaunchIsWindowsHDREnabled = "Launch.IsWindowsHDREnabled";
public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics";
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";
[Obsolete("不再支持多开")]
public const string MultipleInstances = "Launch.MultipleInstances";
[Obsolete("不再使用 PowerShell")]
public const string PowerShellPath = "PowerShellPath";
/// <summary>
/// 空的历史记录卡池是否可见
/// </summary>
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
/// <summary>
/// 窗口背景类型
/// </summary>
public const string SystemBackdropType = "SystemBackdropType";
/// <summary>
/// 启用高级功能
/// </summary>
public const string IsAdvancedLaunchOptionsEnabled = "IsAdvancedLaunchOptionsEnabled";
/// <summary>
/// 实时便笺刷新时间
/// </summary>
public const string DailyNoteRefreshSeconds = "DailyNote.RefreshSeconds";
/// <summary>
/// 实时便笺提醒式通知
/// </summary>
public const string DailyNoteReminderNotify = "DailyNote.ReminderNotify";
/// <summary>
/// 实时便笺免打扰模式
/// </summary>
public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame";
/// <summary>
/// 实时便笺 WebhookUrl
/// </summary>
public const string DailyNoteWebhookUrl = "DailyNote.WebhookUrl";
/// <summary>
/// 启动游戏 总开关
/// </summary>
public const string LaunchIsLaunchOptionsEnabled = "Launch.IsLaunchOptionsEnabled";
/// <summary>
/// 启动游戏 独占全屏
/// </summary>
public const string LaunchIsExclusive = "Launch.IsExclusive";
/// <summary>
/// 启动游戏 全屏
/// </summary>
public const string LaunchIsFullScreen = "Launch.IsFullScreen";
/// <summary>
/// 启动游戏 无边框
/// </summary>
public const string LaunchIsBorderless = "Launch.IsBorderless";
/// <summary>
/// 启动游戏 宽度
/// </summary>
public const string LaunchScreenWidth = "Launch.ScreenWidth";
public const string LaunchIsScreenWidthEnabled = "Launch.IsScreenWidthEnabled";
/// <summary>
/// 启动游戏 高度
/// </summary>
public const string LaunchScreenHeight = "Launch.ScreenHeight";
public const string LaunchIsScreenHeightEnabled = "Launch.IsScreenHeightEnabled";
/// <summary>
/// 启动游戏 解锁帧率
/// </summary>
public const string LaunchUnlockFps = "Launch.UnlockFps";
/// <summary>
/// 启动游戏 目标帧率
/// </summary>
public const string LaunchTargetFps = "Launch.TargetFps";
/// <summary>
/// 启动游戏 显示器编号
/// </summary>
public const string LaunchMonitor = "Launch.Monitor";
public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled";
public const string LaunchIsUseCloudThirdPartyMobile = "Launch.IsUseCloudThirdPartyMobile";
public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics";
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";
/// <summary>
/// 启动游戏 多倍启动
/// </summary>
[Obsolete("不再支持多开")]
public const string MultipleInstances = "Launch.MultipleInstances";
/// <summary>
/// 语言
/// </summary>
public const string Culture = "Culture";
/// <summary>
/// 自定义极验接口
/// </summary>
public const string GeetestCustomCompositeUrl = "GeetestCustomCompositeUrl";
public const string AnnouncementRegion = "AnnouncementRegion";
}

View File

@@ -2192,12 +2192,6 @@
<data name="ViewPageLaunchGameUnlockFpsOn" xml:space="preserve">
<value>启用</value>
</data>
<data name="ViewPageLaunchGameWindowsHDRDescription" xml:space="preserve">
<value>充分利用支持高动态范围的显示器获得更亮、更生动、更精细的画面</value>
</data>
<data name="ViewPageLaunchGameWindowsHDRHeader" xml:space="preserve">
<value>Windows HDR</value>
</data>
<data name="ViewPageLoginHoyoverseUserHint" xml:space="preserve">
<value>请输入你的 HoYoLab Uid</value>
</data>
@@ -2753,9 +2747,6 @@
<data name="WebAnnouncementTimeHoursEndFormat" xml:space="preserve">
<value>{0} 小时后结束</value>
</data>
<data name="WebBridgeShareCopyToClipboardFailed" xml:space="preserve">
<value>打开剪贴板失败</value>
</data>
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
<value>已复制到剪贴板</value>
</data>

View File

@@ -1,10 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Discord.GameSDK;
using Snap.Discord.GameSDK.ABI;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Unicode;
namespace Snap.Hutao.Service.Discord;
@@ -18,94 +18,75 @@ internal static class DiscordController
private static readonly CancellationTokenSource StopTokenSource = new();
private static readonly object SyncRoot = new();
private static long currentClientId;
private static unsafe IDiscordCore* discordCorePtr;
private static Snap.Discord.GameSDK.Discord? discordManager;
private static bool isInitialized;
public static async ValueTask<DiscordResult> SetDefaultActivityAsync(DateTimeOffset startTime)
{
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
return SetDefaultActivity(startTime);
static unsafe DiscordResult SetDefaultActivity(in DateTimeOffset startTime)
public static async ValueTask<Result> SetDefaultActivityAsync(DateTimeOffset startTime)
{
ResetManagerOrIgnore(HutaoAppId);
if (discordCorePtr is null)
if (discordManager is null)
{
return DiscordResult.Ok;
return Result.Ok;
}
IDiscordActivityManager* activityManagerPtr = discordCorePtr->get_activity_manager(discordCorePtr);
ActivityManager activityManager = discordManager.GetActivityManager();
DiscordActivity activity = default;
activity.timestamps.start = startTime.ToUnixTimeSeconds();
SetString(activity.assets.large_image, 128, "icon"u8);
SetString(activity.assets.large_text, 128, SH.AppName);
Activity activity = default;
activity.Timestamps.Start = startTime.ToUnixTimeSeconds();
activity.Assets.LargeImage = "icon";
activity.Assets.LargeText = SH.AppName;
return new DiscordUpdateActivityAsyncAction(activityManagerPtr).WaitUpdateActivity(activity);
}
return await activityManager.UpdateActivityAsync(activity).ConfigureAwait(false);
}
public static async ValueTask<DiscordResult> SetPlayingYuanShenAsync()
{
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
return SetPlayingYuanShen();
static unsafe DiscordResult SetPlayingYuanShen()
public static async ValueTask<Result> SetPlayingYuanShenAsync()
{
ResetManagerOrIgnore(YuanshenId);
if (discordCorePtr is null)
if (discordManager is null)
{
return DiscordResult.Ok;
return Result.Ok;
}
IDiscordActivityManager* activityManagerPtr = discordCorePtr->get_activity_manager(discordCorePtr);
ActivityManager activityManager = discordManager.GetActivityManager();
DiscordActivity activity = default;
SetString(activity.state, 128, SH.FormatServiceDiscordGameLaunchedBy(SH.AppName));
SetString(activity.details, 128, SH.ServiceDiscordGameActivityDetails);
activity.timestamps.start = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
SetString(activity.assets.large_image, 128, "icon"u8);
SetString(activity.assets.large_text, 128, "原神"u8);
SetString(activity.assets.small_image, 128, "app"u8);
SetString(activity.assets.small_text, 128, SH.AppName);
Activity activity = default;
activity.State = SH.FormatServiceDiscordGameLaunchedBy(SH.AppName);
activity.Details = SH.ServiceDiscordGameActivityDetails;
activity.Timestamps.Start = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
activity.Assets.LargeImage = "icon";
activity.Assets.LargeText = "原神";
activity.Assets.SmallImage = "app";
activity.Assets.SmallText = SH.AppName;
return new DiscordUpdateActivityAsyncAction(activityManagerPtr).WaitUpdateActivity(activity);
}
return await activityManager.UpdateActivityAsync(activity).ConfigureAwait(false);
}
public static async ValueTask<DiscordResult> SetPlayingGenshinImpactAsync()
{
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
return SetPlayingGenshinImpact();
static unsafe DiscordResult SetPlayingGenshinImpact()
public static async ValueTask<Result> SetPlayingGenshinImpactAsync()
{
ResetManagerOrIgnore(GenshinImpactId);
if (discordCorePtr is null)
if (discordManager is null)
{
return DiscordResult.Ok;
return Result.Ok;
}
IDiscordActivityManager* activityManagerPtr = discordCorePtr->get_activity_manager(discordCorePtr);
ActivityManager activityManager = discordManager.GetActivityManager();
DiscordActivity activity = default;
SetString(activity.state, 128, SH.FormatServiceDiscordGameLaunchedBy(SH.AppName));
SetString(activity.details, 128, SH.ServiceDiscordGameActivityDetails);
activity.timestamps.start = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
SetString(activity.assets.large_image, 128, "icon"u8);
SetString(activity.assets.large_text, 128, "Genshin Impact"u8);
SetString(activity.assets.small_image, 128, "app"u8);
SetString(activity.assets.small_text, 128, SH.AppName);
Activity activity = default;
activity.State = SH.FormatServiceDiscordGameLaunchedBy(SH.AppName);
activity.Details = SH.ServiceDiscordGameActivityDetails;
activity.Timestamps.Start = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
activity.Assets.LargeImage = "icon";
activity.Assets.LargeText = "Genshin Impact";
activity.Assets.SmallImage = "app";
activity.Assets.SmallText = SH.AppName;
return new DiscordUpdateActivityAsyncAction(activityManagerPtr).WaitUpdateActivity(activity);
}
return await activityManager.UpdateActivityAsync(activity).ConfigureAwait(false);
}
public static unsafe void Stop()
public static void Stop()
{
if (!isInitialized)
{
@@ -117,7 +98,7 @@ internal static class DiscordController
StopTokenSource.Cancel();
try
{
discordCorePtr = default;
discordManager?.Dispose();
}
catch (SEHException)
{
@@ -127,30 +108,23 @@ internal static class DiscordController
private static unsafe void ResetManagerOrIgnore(long clientId)
{
if (currentClientId == clientId)
if (discordManager?.ClientId == clientId)
{
return;
}
// Actually requires a discord client to be running on Windows platform.
// If not, the following creation code will throw.
if (System.Diagnostics.Process.GetProcessesByName("Discord").Length <= 0)
if (System.Diagnostics.Process.GetProcessesByName("Discord").Length == 0)
{
return;
}
lock (SyncRoot)
{
DiscordCreateParams @params = default;
Methods.DiscordCreateParamsSetDefault(&@params);
@params.client_id = clientId;
@params.flags = (uint)DiscordCreateFlags.Default;
IDiscordCore* ptr = default;
Methods.DiscordCreate(3, &@params, &ptr);
currentClientId = clientId;
discordCorePtr = ptr;
discordCorePtr->set_log_hook(discordCorePtr, DiscordLogLevel.Debug, default, &DebugWriteDiscordMessage);
discordManager?.Dispose();
discordManager = new(clientId, CreateFlags.NoRequireDiscord);
discordManager.SetLogHook(Snap.Discord.GameSDK.LogLevel.Debug, SetLogHookHandler.Create(&DebugWriteDiscordMessage));
}
if (isInitialized)
@@ -161,10 +135,10 @@ internal static class DiscordController
DiscordRunCallbacksAsync(StopTokenSource.Token).SafeForget();
isInitialized = true;
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
static unsafe void DebugWriteDiscordMessage(void* state, DiscordLogLevel logLevel, sbyte* ptr)
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
static unsafe void DebugWriteDiscordMessage(Snap.Discord.GameSDK.LogLevel logLevel, byte* ptr)
{
ReadOnlySpan<byte> utf8 = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr);
ReadOnlySpan<byte> utf8 = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(ptr);
string message = System.Text.Encoding.UTF8.GetString(utf8);
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK]:[{logLevel}]:{message}");
}
@@ -172,7 +146,7 @@ internal static class DiscordController
private static async ValueTask DiscordRunCallbacksAsync(CancellationToken cancellationToken)
{
using (PeriodicTimer timer = new(TimeSpan.FromMilliseconds(500)))
using (PeriodicTimer timer = new(TimeSpan.FromMilliseconds(1000)))
{
try
{
@@ -187,10 +161,15 @@ internal static class DiscordController
{
try
{
DiscordResult result = DiscordCoreRunRunCallbacks();
if (result is not DiscordResult.Ok)
discordManager?.RunCallbacks();
}
catch (ResultException ex)
{
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{result:D} {result}");
// If result is Ok
// Maybe the connection is reset.
if (ex.Result is not Result.Ok)
{
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{ex.Result:D} {ex.Result}");
}
}
catch (SEHException ex)
@@ -206,70 +185,5 @@ internal static class DiscordController
{
}
}
unsafe DiscordResult DiscordCoreRunRunCallbacks()
{
if (discordCorePtr is not null)
{
return discordCorePtr->run_callbacks(discordCorePtr);
}
return DiscordResult.Ok;
}
}
private static unsafe void SetString(sbyte* reference, int length, string source)
{
Span<sbyte> sbytes = new(reference, length);
sbytes.Clear();
Utf8.FromUtf16(source.AsSpan(), MemoryMarshal.Cast<sbyte, byte>(sbytes), out _, out _);
}
private static unsafe void SetString(sbyte* reference, int length, in ReadOnlySpan<byte> source)
{
Span<sbyte> sbytes = new(reference, length);
sbytes.Clear();
source.CopyTo(MemoryMarshal.Cast<sbyte, byte>(sbytes));
}
private struct DiscordAsyncAction
{
public DiscordResult Result;
public bool IsCompleted;
}
private unsafe struct DiscordUpdateActivityAsyncAction
{
private readonly IDiscordActivityManager* activityManagerPtr;
private DiscordAsyncAction discordAsyncAction;
public DiscordUpdateActivityAsyncAction(IDiscordActivityManager* activityManagerPtr)
{
this.activityManagerPtr = activityManagerPtr;
}
public DiscordResult WaitUpdateActivity(DiscordActivity activity)
{
fixed (DiscordAsyncAction* actionPtr = &discordAsyncAction)
{
activityManagerPtr->update_activity(activityManagerPtr, &activity, actionPtr, &HandleResult);
}
SpinWaitPolyfill.SpinUntil(ref discordAsyncAction, &CheckActionCompleted);
return discordAsyncAction.Result;
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void HandleResult(void* state, DiscordResult result)
{
DiscordAsyncAction* action = (DiscordAsyncAction*)state;
action->Result = result;
action->IsCompleted = true;
}
private static bool CheckActionCompleted(ref readonly DiscordAsyncAction state)
{
return state.IsCompleted;
}
}
}

View File

@@ -20,8 +20,6 @@ internal static class RegistryInterop
private const string SdkChineseValueName = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
private const string SdkOverseaValueName = "MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810";
private const string WindowsHDROnValueName = "WINDOWS_HDR_ON_h3132281285";
public static bool Set(GameAccount? account)
{
if (account is not null)
@@ -58,12 +56,6 @@ internal static class RegistryInterop
}
}
public static void SetWindowsHDR(bool isOversea)
{
string keyName = isOversea ? OverseaKeyName : ChineseKeyName;
Registry.SetValue(keyName, WindowsHDROnValueName, 1);
}
private static (string KeyName, string ValueName) GetKeyValueName(SchemeType scheme)
{
return scheme switch

View File

@@ -42,7 +42,6 @@ internal sealed class LaunchOptions : DbStoreOptions
private NameValue<int>? monitor;
private bool? isMonitorEnabled;
private bool? isUseCloudThirdPartyMobile;
private bool? isWindowsHDREnabled;
private AspectRatio? selectedAspectRatio;
private bool? useStarwardPlayTimeStatistics;
private bool? setDiscordActivityWhenPlaying;
@@ -199,12 +198,6 @@ internal sealed class LaunchOptions : DbStoreOptions
set => SetOption(ref isUseCloudThirdPartyMobile, SettingEntry.LaunchIsUseCloudThirdPartyMobile, value);
}
public bool IsWindowsHDREnabled
{
get => GetOption(ref isWindowsHDREnabled, SettingEntry.LaunchIsWindowsHDREnabled, false);
set => SetOption(ref isWindowsHDREnabled, SettingEntry.LaunchIsWindowsHDREnabled, value);
}
public List<AspectRatio> AspectRatios { get; } =
[
new(2560, 1440),

View File

@@ -4,7 +4,6 @@
using Snap.Hutao.Core;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Service.Discord;
using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Game.Unlocker;
using System.IO;
@@ -62,11 +61,6 @@ internal sealed partial class GameProcessService : IGameProcessService
bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileName);
if (launchOptions.IsWindowsHDREnabled)
{
RegistryInterop.SetWindowsHDR(isOversea);
}
progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
using (System.Diagnostics.Process game = InitializeGameProcess(gamePath))
{

View File

@@ -311,7 +311,7 @@
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.6.0" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.5.0" />
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.9.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

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

View File

@@ -199,12 +199,6 @@
ItemsSource="{Binding GameAccountsView}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</Border>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageLaunchGameWindowsHDRDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameWindowsHDRHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE7F7;}">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsWindowsHDREnabled, Mode=TwoWay}"/>
</cwc:SettingsCard>
<!-- 进程 -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameProcessHeader}"/>

View File

@@ -3,9 +3,7 @@
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Bridge.Model;
@@ -14,12 +12,8 @@ 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.IO;
using System.Net.Http;
using System.Text;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace Snap.Hutao.Web.Bridge;
@@ -30,7 +24,7 @@ namespace Snap.Hutao.Web.Bridge;
[SuppressMessage("", "CA1001")]
internal class MiHoYoJSBridge
{
private const string InitializeJsInterfaceScript = """
private const string InitializeJsInterfaceScript2 = """
window.MiHoYoJSInterface = {
postMessage: function(arg) { chrome.webview.postMessage(arg) },
closePage: function() { this.postMessage('{"method":"closePage"}') },
@@ -45,6 +39,7 @@ internal class MiHoYoJSBridge
private const string ConvertMouseToTouchScript = """
function mouseListener (e, event) {
console.log(event);
let touch = new Touch({
identifier: Date.now(),
target: e.target,
@@ -62,6 +57,7 @@ internal class MiHoYoJSBridge
targetTouches: [touch],
changedTouches: [touch],
});
console.log(touchEvent);
e.target.dispatchEvent(touchEvent);
}
@@ -90,9 +86,6 @@ internal class MiHoYoJSBridge
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly ILogger<MiHoYoJSBridge> logger;
private readonly IInfoBarService infoBarService;
private readonly IClipboardProvider clipboardProvider;
private readonly HttpClient httpClient;
private readonly TypedEventHandler<CoreWebView2, CoreWebView2WebMessageReceivedEventArgs> webMessageReceivedEventHandler;
private readonly TypedEventHandler<CoreWebView2, CoreWebView2DOMContentLoadedEventArgs> domContentLoadedEventHandler;
@@ -109,9 +102,6 @@ internal class MiHoYoJSBridge
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
logger = serviceProvider.GetRequiredService<ILogger<MiHoYoJSBridge>>();
infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
clipboardProvider = serviceProvider.GetRequiredService<IClipboardProvider>();
httpClient = serviceProvider.GetRequiredService<HttpClient>();
webMessageReceivedEventHandler = OnWebMessageReceived;
domContentLoadedEventHandler = OnDOMContentLoaded;
@@ -374,51 +364,8 @@ internal class MiHoYoJSBridge
return null;
}
protected virtual async ValueTask<IJsBridgeResult?> Share(JsParam<SharePayload> param)
protected virtual IJsBridgeResult? Share(JsParam<SharePayload> param)
{
if (param.Payload.Type is "image")
{
if (param.Payload.Content.ImageUrl is { } imageUrl)
{
using (Stream stream = await httpClient.GetStreamAsync(imageUrl).ConfigureAwait(false))
{
using (InMemoryRandomAccessStream origStream = new())
{
await stream.CopyToAsync(origStream.AsStreamForWrite()).ConfigureAwait(false);
using (InMemoryRandomAccessStream imageStream = new())
{
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, imageStream);
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(origStream);
encoder.SetSoftwareBitmap(await decoder.GetSoftwareBitmapAsync());
await encoder.FlushAsync();
await taskContext.SwitchToMainThreadAsync();
if (clipboardProvider.SetBitmap(imageStream))
{
infoBarService.Success(SH.WebBridgeShareCopyToClipboardSuccess);
}
else
{
infoBarService.Error(SH.WebBridgeShareCopyToClipboardFailed);
}
}
}
}
}
else if (param.Payload.Content.ImageBase64 is { } imageBase64)
{
await ShareImageBase64CoreAsync(imageBase64).ConfigureAwait(false);
}
}
else if (param.Payload.Type is "screenshot")
{
await taskContext.SwitchToMainThreadAsync();
string base64Json = await coreWebView2.CallDevToolsProtocolMethodAsync("Page.captureScreenshot", """{"format":"png","captureBeyondViewport":true}""");
string? base64 = JsonDocument.Parse(base64Json).RootElement.GetProperty("data").GetString();
ArgumentNullException.ThrowIfNull(base64);
await ShareImageBase64CoreAsync(base64).ConfigureAwait(false);
}
return new JsResult<Dictionary<string, string>>()
{
Data = new()
@@ -428,30 +375,6 @@ internal class MiHoYoJSBridge
};
}
private async ValueTask ShareImageBase64CoreAsync(string base64)
{
using (MemoryStream imageStream = new())
{
await imageStream.WriteAsync(Convert.FromBase64String(base64)).ConfigureAwait(false);
using (InMemoryRandomAccessStream stream = new())
{
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream.AsRandomAccessStream());
encoder.SetSoftwareBitmap(await decoder.GetSoftwareBitmapAsync());
await encoder.FlushAsync();
await taskContext.SwitchToMainThreadAsync();
if (clipboardProvider.SetBitmap(stream))
{
infoBarService.Success(SH.WebBridgeShareCopyToClipboardSuccess);
}
else
{
infoBarService.Error(SH.WebBridgeShareCopyToClipboardFailed);
}
}
}
}
protected virtual ValueTask<IJsBridgeResult?> ShowAlertDialogAsync(JsParam param)
{
return ValueTask.FromException<IJsBridgeResult?>(new NotSupportedException());
@@ -582,7 +505,7 @@ internal class MiHoYoJSBridge
"hideLoading" => null,
"login" => null,
"pushPage" => await PushPageAsync(param).ConfigureAwait(false),
"share" => await Share(param).ConfigureAwait(false),
"share" => Share(param),
"showLoading" => null,
_ => LogUnhandledMessage("Unhandled Message Type: {Method}", param.Method),
};
@@ -607,7 +530,7 @@ internal class MiHoYoJSBridge
ReadOnlySpan<char> uriHostSpan = uriHost.AsSpan();
if (uriHostSpan.EndsWith("mihoyo.com") || uriHostSpan.EndsWith("hoyolab.com"))
{
coreWebView2.ExecuteScriptAsync(InitializeJsInterfaceScript).AsTask().SafeForget(logger);
coreWebView2.ExecuteScriptAsync(InitializeJsInterfaceScript2).AsTask().SafeForget(logger);
}
}
}

View File

@@ -3,23 +3,8 @@
namespace Snap.Hutao.Web.Bridge.Model;
[SuppressMessage("", "SA1124")]
internal sealed class ShareContent
{
#region Screenshot
[JsonPropertyName("preview")]
public bool Preview { get; set; }
#endregion
#region Image
[JsonPropertyName("image_url")]
public string? ImageUrl { get; set; }
[JsonPropertyName("image_base64")]
public string? ImageBase64 { get; set; }
#endregion
}

View File

@@ -3,4 +3,6 @@
namespace Snap.Hutao.Web.Request.Builder.Abstraction;
internal interface IBuilder;
internal interface IBuilder
{
}