diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
index 87c69eda..490dff8e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
+++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
@@ -2753,6 +2753,9 @@
{0} 小时后结束
+
+ 打开剪贴板失败
+
已复制到剪贴板
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/BridgeShareContext.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/BridgeShareContext.cs
new file mode 100644
index 00000000..7a7ea616
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/BridgeShareContext.cs
@@ -0,0 +1,41 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.Web.WebView2.Core;
+using Snap.Hutao.Core.IO.DataTransfer;
+using Snap.Hutao.Service.Notification;
+using System.Net.Http;
+
+namespace Snap.Hutao.Web.Bridge;
+
+internal sealed class BridgeShareContext
+{
+ private readonly CoreWebView2 coreWebView2;
+ private readonly ITaskContext taskContext;
+ private readonly HttpClient httpClient;
+ private readonly IInfoBarService infoBarService;
+ private readonly IClipboardProvider clipboardProvider;
+ private readonly JsonSerializerOptions jsonSerializerOptions;
+
+ public BridgeShareContext(CoreWebView2 coreWebView2, ITaskContext taskContext, HttpClient httpClient, IInfoBarService infoBarService, IClipboardProvider clipboardProvider, JsonSerializerOptions jsonSerializerOptions)
+ {
+ this.httpClient = httpClient;
+ this.taskContext = taskContext;
+ this.infoBarService = infoBarService;
+ this.clipboardProvider = clipboardProvider;
+ this.coreWebView2 = coreWebView2;
+ this.jsonSerializerOptions = jsonSerializerOptions;
+ }
+
+ public CoreWebView2 CoreWebView2 { get => coreWebView2; }
+
+ public ITaskContext TaskContext { get => taskContext; }
+
+ public HttpClient HttpClient { get => httpClient; }
+
+ public IInfoBarService InfoBarService { get => infoBarService; }
+
+ public IClipboardProvider ClipboardProvider { get => clipboardProvider; }
+
+ public JsonSerializerOptions JsonSerializerOptions { get => jsonSerializerOptions; }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/BridgeShareImplmentation.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/BridgeShareImplmentation.cs
new file mode 100644
index 00000000..f3aeae0c
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/BridgeShareImplmentation.cs
@@ -0,0 +1,108 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Service.Notification;
+using Snap.Hutao.Web.Bridge.Model;
+using System.IO;
+using Windows.Graphics.Imaging;
+using Windows.Storage.Streams;
+
+namespace Snap.Hutao.Web.Bridge;
+
+internal sealed partial class BridgeShareImplmentation
+{
+ public static async ValueTask ShareAsync(JsParam param, BridgeShareContext context)
+ {
+ SharePayload payload = param.Payload;
+ switch (payload.Type)
+ {
+ case "image":
+ {
+ ShareContent content = payload.Content;
+ if (content.ImageUrl is { Length: > 0 } imageUrl)
+ {
+ await ShareFromImageUrlAsync(context, imageUrl).ConfigureAwait(false);
+ }
+ else if (content.ImageBase64 is { } imageBase64)
+ {
+ await ShareFromImageBase64Async(context, imageBase64).ConfigureAwait(false);
+ }
+
+ break;
+ }
+
+ case "screenshot":
+ {
+ await context.TaskContext.SwitchToMainThreadAsync();
+
+ // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot
+ string jsonParameters = """{ "format": "png", "captureBeyondViewport": true }""";
+ string resultJson = await context.CoreWebView2.CallDevToolsProtocolMethodAsync("Page.captureScreenshot", jsonParameters);
+
+ CaptureScreenshotResult? result = JsonSerializer.Deserialize(resultJson, context.JsonSerializerOptions);
+ ArgumentNullException.ThrowIfNull(result);
+
+ await ShareFromRawPixelDataAsync(context, result.Data).ConfigureAwait(false);
+ break;
+ }
+ }
+
+ return new JsResult>()
+ {
+ Data = new()
+ {
+ ["type"] = param.Payload.Type,
+ },
+ };
+ }
+
+ private static async ValueTask ShareFromImageUrlAsync(BridgeShareContext context, string imageUrl)
+ {
+ using (Stream stream = await context.HttpClient.GetStreamAsync(imageUrl).ConfigureAwait(false))
+ {
+ await ShareCoreAsync(context, stream, static (stream, web) => web.CopyToAsync(stream.AsStreamForWrite())).ConfigureAwait(false);
+ }
+ }
+
+ private static ValueTask ShareFromImageBase64Async(BridgeShareContext context, string base64ImageData)
+ {
+ return ShareFromRawPixelDataAsync(context, Convert.FromBase64String(base64ImageData));
+ }
+
+ private static ValueTask ShareFromRawPixelDataAsync(BridgeShareContext context, byte[] rawPixelData)
+ {
+ return ShareCoreAsync(context, rawPixelData, static (stream, bytes) => stream.AsStreamForWrite().WriteAsync(bytes).AsTask());
+ }
+
+ private static async ValueTask ShareCoreAsync(BridgeShareContext context, TData data, Func asyncWriteData)
+ {
+ using (InMemoryRandomAccessStream rawPixelDataStream = new())
+ {
+ await asyncWriteData(rawPixelDataStream, data).ConfigureAwait(false);
+ BitmapDecoder decoder = await BitmapDecoder.CreateAsync(rawPixelDataStream);
+
+ using (InMemoryRandomAccessStream stream = new())
+ {
+ BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
+ encoder.SetSoftwareBitmap(await decoder.GetSoftwareBitmapAsync());
+ await encoder.FlushAsync();
+
+ await context.TaskContext.SwitchToMainThreadAsync();
+ if (context.ClipboardProvider.SetBitmap(stream))
+ {
+ context.InfoBarService.Success(SH.WebBridgeShareCopyToClipboardSuccess);
+ }
+ else
+ {
+ context.InfoBarService.Error(SH.WebBridgeShareCopyToClipboardFailed);
+ }
+ }
+ }
+ }
+
+ private sealed class CaptureScreenshotResult
+ {
+ [JsonPropertyName("data")]
+ public byte[] Data { get; set; } = default!;
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs
index b2561573..21307c6b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs
@@ -3,7 +3,9 @@
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;
@@ -12,8 +14,12 @@ 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;
@@ -78,7 +84,7 @@ internal class MiHoYoJSBridge
""";
private readonly SemaphoreSlim webMessageSemaphore = new(1);
- private readonly Guid interfaceId = Guid.NewGuid();
+ private readonly Guid bridgeId = Guid.NewGuid();
private readonly UserAndUid userAndUid;
private readonly IServiceProvider serviceProvider;
@@ -120,11 +126,6 @@ internal class MiHoYoJSBridge
coreWebView2 = default!;
}
- ///
- /// 关闭
- ///
- /// 参数
- /// 响应
protected virtual async ValueTask ClosePageAsync(JsParam param)
{
await taskContext.SwitchToMainThreadAsync();
@@ -140,21 +141,11 @@ internal class MiHoYoJSBridge
return null;
}
- ///
- /// 调整分享设置
- ///
- /// 参数
- /// 响应
protected virtual IJsBridgeResult? ConfigureShare(JsParam param)
{
return null;
}
- ///
- /// 获取ActionTicket
- ///
- /// 参数
- /// 响应
protected virtual async ValueTask GetActionTicketAsync(JsParam jsParam)
{
return await serviceProvider
@@ -163,11 +154,6 @@ internal class MiHoYoJSBridge
.ConfigureAwait(false);
}
- ///
- /// 异步获取账户信息
- ///
- /// 参数
- /// 响应
protected virtual JsResult> GetCookieInfo(JsParam param)
{
ArgumentNullException.ThrowIfNull(userAndUid.User.LToken);
@@ -183,11 +169,6 @@ internal class MiHoYoJSBridge
};
}
- ///
- /// 获取CookieToken
- ///
- /// 参数
- /// 响应
protected virtual async ValueTask>> GetCookieTokenAsync(JsParam param)
{
IUserService userService = serviceProvider.GetRequiredService();
@@ -203,11 +184,6 @@ internal class MiHoYoJSBridge
return new() { Data = new() { [Cookie.COOKIE_TOKEN] = userAndUid.User.CookieToken[Cookie.COOKIE_TOKEN] } };
}
- ///
- /// 获取当前语言和时区
- ///
- /// param
- /// 语言与时区
protected virtual JsResult> GetCurrentLocale(JsParam param)
{
MetadataOptions metadataOptions = serviceProvider.GetRequiredService();
@@ -222,11 +198,6 @@ internal class MiHoYoJSBridge
};
}
- ///
- /// 获取1代动态密钥
- ///
- /// 参数
- /// 响应
protected virtual JsResult> GetDynamicSecrectV1(JsParam param)
{
DataSignOptions options = DataSignOptions.CreateForGeneration1(SaltType.LK2, true);
@@ -239,11 +210,6 @@ internal class MiHoYoJSBridge
};
}
- ///
- /// 获取2代动态密钥
- ///
- /// 参数
- /// 响应
protected virtual JsResult> GetDynamicSecrectV2(JsParam param)
{
DataSignOptions options = DataSignOptions.CreateForGeneration2(SaltType.X4, false, param.Payload.Body, param.Payload.GetQueryParam());
@@ -256,11 +222,6 @@ internal class MiHoYoJSBridge
};
}
- ///
- /// 获取Http请求头
- ///
- /// 参数
- /// Http请求头
protected virtual JsResult> GetHttpRequestHeader(JsParam param)
{
Dictionary headers = new()
@@ -294,21 +255,11 @@ internal class MiHoYoJSBridge
{
}
- ///
- /// 获取状态栏高度
- ///
- /// 参数
- /// 结果
protected virtual JsResult> GetStatusBarHeight(JsParam param)
{
return new() { Data = new() { ["statusBarHeight"] = 0 } };
}
- ///
- /// 获取用户基本信息
- ///
- /// 参数
- /// 响应
protected virtual async ValueTask>> GetUserInfoAsync(JsParam param)
{
Response response = await serviceProvider
@@ -362,15 +313,19 @@ internal class MiHoYoJSBridge
return null;
}
- protected virtual IJsBridgeResult? Share(JsParam param)
+ protected virtual async ValueTask ShareAsync(JsParam param)
{
- return new JsResult>()
+ using (IServiceScope scope = serviceProvider.CreateScope())
{
- Data = new()
- {
- ["type"] = param.Payload.Type,
- },
- };
+ JsonSerializerOptions jsonSerializerOptions = scope.ServiceProvider.GetRequiredService();
+ HttpClient httpClient = scope.ServiceProvider.GetRequiredService();
+ IClipboardProvider clipboardProvider = scope.ServiceProvider.GetRequiredService();
+ IInfoBarService infoBarService = scope.ServiceProvider.GetRequiredService();
+
+ BridgeShareContext context = new(coreWebView2, taskContext, httpClient, infoBarService, clipboardProvider, jsonSerializerOptions);
+
+ return await BridgeShareImplmentation.ShareAsync(param, context).ConfigureAwait(false);
+ }
}
protected virtual ValueTask ShowAlertDialogAsync(JsParam param)
@@ -440,7 +395,7 @@ internal class MiHoYoJSBridge
.Append(')')
.ToString();
- logger?.LogInformation("[{Id}][ExecuteScript: {callback}]\n{payload}", interfaceId, callback, payload);
+ logger?.LogInformation("[{Id}][ExecuteScript: {callback}]\n{payload}", bridgeId, callback, payload);
await taskContext.SwitchToMainThreadAsync();
if (coreWebView2 is null || coreWebView2.IsDisposed())
@@ -454,7 +409,7 @@ internal class MiHoYoJSBridge
private async void OnWebMessageReceived(CoreWebView2 webView2, CoreWebView2WebMessageReceivedEventArgs args)
{
string message = args.TryGetWebMessageAsString();
- logger.LogInformation("[{Id}][OnRawMessage]\n{message}", interfaceId, message);
+ logger.LogInformation("[{Id}][OnRawMessage]\n{message}", bridgeId, message);
JsParam? param = JsonSerializer.Deserialize(message);
ArgumentNullException.ThrowIfNull(param);
@@ -503,7 +458,7 @@ internal class MiHoYoJSBridge
"hideLoading" => null,
"login" => null,
"pushPage" => await PushPageAsync(param).ConfigureAwait(false),
- "share" => Share(param),
+ "share" => await ShareAsync(param).ConfigureAwait(false),
"showLoading" => null,
_ => LogUnhandledMessage("Unhandled Message Type: {Method}", param.Method),
};
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/ShareContent.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/ShareContent.cs
index e7764da3..efc607f9 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/ShareContent.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/ShareContent.cs
@@ -3,8 +3,15 @@
namespace Snap.Hutao.Web.Bridge.Model;
+[SuppressMessage("", "SA1124")]
internal sealed class ShareContent
{
[JsonPropertyName("preview")]
public bool Preview { get; set; }
-}
\ No newline at end of file
+
+ [JsonPropertyName("image_url")]
+ public string? ImageUrl { get; set; }
+
+ [JsonPropertyName("image_base64")]
+ public string? ImageBase64 { get; set; }
+}