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; } +}