From fcd0b65257100e583d12840d4c70fc727999bef9 Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Tue, 2 Jan 2024 21:14:48 +0800 Subject: [PATCH 1/4] impl #1244 --- .../Snap.Hutao/Core/IO/ValueFileExtensions.cs | 54 +++++++++++ src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 1 + .../Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs | 91 ++++++++++++++++++- .../Web/Bridge/Model/ShareContent.cs | 17 +++- 4 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/ValueFileExtensions.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/ValueFileExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/ValueFileExtensions.cs new file mode 100644 index 00000000..8ec238bd --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/ValueFileExtensions.cs @@ -0,0 +1,54 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.IO; +using System.Net.Http; + +namespace Snap.Hutao.Core.IO; + +internal static class ValueFileExtensions +{ + public static async ValueTask DownloadAsync(this ValueFile valueFile, string url) + { + try + { + using (HttpClient httpClient = new()) + { + using (HttpResponseMessage response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false)) + { + long contentLength = response.Content.Headers.ContentLength ?? 0; + using (Stream content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + using (FileStream stream = File.Create((string)valueFile)) + { + Progress progress = new(); + await new StreamCopyWorker(content, stream, contentLength).CopyAsync(progress).ConfigureAwait(false); + return true; + } + } + } + } + } + catch (Exception) + { + return false; + } + } + + public static async ValueTask WritePngBase64Async(this ValueFile valueFile, string base64) + { + try + { + using (FileStream stream = File.Create((string)valueFile)) + { + byte[] bytes = System.Convert.FromBase64String(base64); + await stream.WriteAsync(bytes).ConfigureAwait(false); + return true; + } + } + catch (Exception) + { + return false; + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index d22d2b04..ee4a78f5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -310,6 +310,7 @@ + diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs index b2561573..4513be0a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs @@ -2,8 +2,12 @@ // Licensed under the MIT license. using Microsoft.Web.WebView2.Core; +using Qhy04.WebView2.DevTools.Protocol.WinUI3; using Snap.Hutao.Core.DependencyInjection.Abstraction; +using Snap.Hutao.Core.IO; +using Snap.Hutao.Factory.Picker; 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; @@ -84,6 +88,7 @@ internal class MiHoYoJSBridge private readonly IServiceProvider serviceProvider; private readonly ITaskContext taskContext; private readonly ILogger logger; + private readonly DevToolsProtocolHelper devToolsProtocolHelper; private readonly TypedEventHandler webMessageReceivedEventHandler; private readonly TypedEventHandler domContentLoadedEventHandler; @@ -100,6 +105,7 @@ internal class MiHoYoJSBridge taskContext = serviceProvider.GetRequiredService(); logger = serviceProvider.GetRequiredService>(); + devToolsProtocolHelper = coreWebView2.GetDevToolsProtocolHelper(); webMessageReceivedEventHandler = OnWebMessageReceived; domContentLoadedEventHandler = OnDOMContentLoaded; @@ -362,8 +368,89 @@ internal class MiHoYoJSBridge return null; } - protected virtual IJsBridgeResult? Share(JsParam param) + protected virtual async ValueTask Share(JsParam param) { + IInfoBarService infoBarService = serviceProvider.GetRequiredService(); + IFileSystemPickerInteraction fileSystemPickerInteraction = serviceProvider.GetRequiredService(); + + string shareType = param.Payload.Type; + ShareContent shareContent = param.Payload.Content; + if (shareType == "image") + { + if (shareContent.ImageUrl is not null) + { + string fileName = shareContent.ImageUrl.Split("/").Last(); + string format = fileName.Split(".").Last(); + + (bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile( + "保存分享图片", + fileName, + [("图片文件", format)]); + + if (isOk) + { + if (await file.DownloadAsync(shareContent.ImageUrl).ConfigureAwait(false)) + { + infoBarService.Success("保存成功", "成功保存到指定位置"); + } + else + { + infoBarService.Error("保存失败", "无法保存到指定位置"); + } + } + } + else if (shareContent.ImageBase64 is not null) + { + string fileName = "image.png"; + string format = "png"; + + (bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile( + "保存分享图片", + fileName, + [("图片文件", format)]); + + if (isOk) + { + if (await file.WritePngBase64Async(shareContent.ImageBase64).ConfigureAwait(false)) + { + infoBarService.Success("保存成功", "成功保存到指定位置"); + } + else + { + infoBarService.Error("保存失败", "无法保存到指定位置"); + } + } + } + } + else if (shareType == "screenshot") + { + string fileName = "image.png"; + string format = "png"; + + (bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile( + "保存分享图片", + fileName, + [("图片文件", format)]); + + if (isOk) + { + if (shareContent.Preview) + { + await taskContext.SwitchToMainThreadAsync(); + string base64 = await devToolsProtocolHelper.Page.CaptureScreenshotAsync(format: "png", captureBeyondViewport: true).ConfigureAwait(false); + + if (await file.WritePngBase64Async(base64).ConfigureAwait(false)) + { + infoBarService.Success("保存成功", "成功保存到指定位置"); + } + else + { + infoBarService.Error("保存失败", "无法保存到指定位置"); + } + } + } + } + return new JsResult>() { Data = new() @@ -503,7 +590,7 @@ internal class MiHoYoJSBridge "hideLoading" => null, "login" => null, "pushPage" => await PushPageAsync(param).ConfigureAwait(false), - "share" => Share(param), + "share" => await Share(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..26437013 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,23 @@ namespace Snap.Hutao.Web.Bridge.Model; +[SuppressMessage("", "SA1124")] internal sealed class ShareContent { + #region Screenshot + [JsonPropertyName("preview")] public bool Preview { get; set; } -} \ No newline at end of file + + #endregion + + #region Image + + [JsonPropertyName("image_url")] + public string? ImageUrl { get; set; } + + [JsonPropertyName("image_base64")] + public string? ImageBase64 { get; set; } + + #endregion +} From d6b79584b6880a669de5d3f64a2e78476c35e414 Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Tue, 2 Jan 2024 23:21:11 +0800 Subject: [PATCH 2/4] streams need rework and resx --- .../Snap.Hutao/Core/IO/ValueFileExtensions.cs | 54 ----------- src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 1 - .../Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs | 96 +++++++++---------- 3 files changed, 45 insertions(+), 106 deletions(-) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/ValueFileExtensions.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/ValueFileExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/ValueFileExtensions.cs deleted file mode 100644 index 8ec238bd..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/ValueFileExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using System.IO; -using System.Net.Http; - -namespace Snap.Hutao.Core.IO; - -internal static class ValueFileExtensions -{ - public static async ValueTask DownloadAsync(this ValueFile valueFile, string url) - { - try - { - using (HttpClient httpClient = new()) - { - using (HttpResponseMessage response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false)) - { - long contentLength = response.Content.Headers.ContentLength ?? 0; - using (Stream content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) - { - using (FileStream stream = File.Create((string)valueFile)) - { - Progress progress = new(); - await new StreamCopyWorker(content, stream, contentLength).CopyAsync(progress).ConfigureAwait(false); - return true; - } - } - } - } - } - catch (Exception) - { - return false; - } - } - - public static async ValueTask WritePngBase64Async(this ValueFile valueFile, string base64) - { - try - { - using (FileStream stream = File.Create((string)valueFile)) - { - byte[] bytes = System.Convert.FromBase64String(base64); - await stream.WriteAsync(bytes).ConfigureAwait(false); - return true; - } - } - catch (Exception) - { - return false; - } - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index ee4a78f5..d22d2b04 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -310,7 +310,6 @@ - diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs index 4513be0a..b382bec1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs @@ -2,10 +2,8 @@ // Licensed under the MIT license. using Microsoft.Web.WebView2.Core; -using Qhy04.WebView2.DevTools.Protocol.WinUI3; using Snap.Hutao.Core.DependencyInjection.Abstraction; -using Snap.Hutao.Core.IO; -using Snap.Hutao.Factory.Picker; +using Snap.Hutao.Core.IO.DataTransfer; using Snap.Hutao.Service.Metadata; using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.User; @@ -16,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; @@ -88,7 +90,6 @@ internal class MiHoYoJSBridge private readonly IServiceProvider serviceProvider; private readonly ITaskContext taskContext; private readonly ILogger logger; - private readonly DevToolsProtocolHelper devToolsProtocolHelper; private readonly TypedEventHandler webMessageReceivedEventHandler; private readonly TypedEventHandler domContentLoadedEventHandler; @@ -105,7 +106,6 @@ internal class MiHoYoJSBridge taskContext = serviceProvider.GetRequiredService(); logger = serviceProvider.GetRequiredService>(); - devToolsProtocolHelper = coreWebView2.GetDevToolsProtocolHelper(); webMessageReceivedEventHandler = OnWebMessageReceived; domContentLoadedEventHandler = OnDOMContentLoaded; @@ -371,81 +371,75 @@ internal class MiHoYoJSBridge protected virtual async ValueTask Share(JsParam param) { IInfoBarService infoBarService = serviceProvider.GetRequiredService(); - IFileSystemPickerInteraction fileSystemPickerInteraction = serviceProvider.GetRequiredService(); + IClipboardProvider clipboardProvider = serviceProvider.GetRequiredService(); string shareType = param.Payload.Type; ShareContent shareContent = param.Payload.Content; - if (shareType == "image") + if (shareType is "image") { if (shareContent.ImageUrl is not null) { - string fileName = shareContent.ImageUrl.Split("/").Last(); - string format = fileName.Split(".").Last(); - - (bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile( - "保存分享图片", - fileName, - [("图片文件", format)]); - - if (isOk) + using (HttpClient httpClient = new()) { - if (await file.DownloadAsync(shareContent.ImageUrl).ConfigureAwait(false)) + using (Stream stream = await httpClient.GetStreamAsync(shareContent.ImageUrl).ConfigureAwait(false)) { - infoBarService.Success("保存成功", "成功保存到指定位置"); - } - else - { - infoBarService.Error("保存失败", "无法保存到指定位置"); + 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(); + if (clipboardProvider.SetBitmap(imageStream)) + { + infoBarService.Success("复制成功", "图片已复制到剪贴板"); + } + else + { + infoBarService.Error("复制失败", string.Empty); + } + } + } } } } else if (shareContent.ImageBase64 is not null) { - string fileName = "image.png"; - string format = "png"; - - (bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile( - "保存分享图片", - fileName, - [("图片文件", format)]); - - if (isOk) + using (MemoryStream imageStream = new()) { - if (await file.WritePngBase64Async(shareContent.ImageBase64).ConfigureAwait(false)) + await imageStream.WriteAsync(Convert.FromBase64String(shareContent.ImageBase64)).ConfigureAwait(false); + if (clipboardProvider.SetBitmap(imageStream.AsRandomAccessStream())) { - infoBarService.Success("保存成功", "成功保存到指定位置"); + infoBarService.Success("复制成功", "图片已复制到剪贴板"); } else { - infoBarService.Error("保存失败", "无法保存到指定位置"); + infoBarService.Error("复制失败", string.Empty); } } } } - else if (shareType == "screenshot") + else if (shareType is "screenshot") { - string fileName = "image.png"; - string format = "png"; - - (bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile( - "保存分享图片", - fileName, - [("图片文件", format)]); - - if (isOk) + if (shareContent.Preview) { - if (shareContent.Preview) - { - await taskContext.SwitchToMainThreadAsync(); - string base64 = await devToolsProtocolHelper.Page.CaptureScreenshotAsync(format: "png", captureBeyondViewport: true).ConfigureAwait(false); + 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); - if (await file.WritePngBase64Async(base64).ConfigureAwait(false)) + using (MemoryStream imageStream = new()) + { + await imageStream.WriteAsync(Convert.FromBase64String(base64)).ConfigureAwait(false); + if (clipboardProvider.SetBitmap(imageStream.AsRandomAccessStream())) { - infoBarService.Success("保存成功", "成功保存到指定位置"); + infoBarService.Success("复制成功", "图片已复制到剪贴板"); } else { - infoBarService.Error("保存失败", "无法保存到指定位置"); + infoBarService.Error("复制失败", string.Empty); } } } From 104fb9a3b0f163fc11e402ecff31a133a695298b Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Wed, 3 Jan 2024 10:43:36 +0800 Subject: [PATCH 3/4] finish up --- .../Snap.Hutao/Resource/Localization/SH.resx | 3 + .../Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs | 112 +++++++++--------- 2 files changed, 58 insertions(+), 57 deletions(-) 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/MiHoYoJSBridge.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs index b382bec1..c900adf6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs @@ -90,6 +90,9 @@ internal class MiHoYoJSBridge private readonly IServiceProvider serviceProvider; private readonly ITaskContext taskContext; private readonly ILogger logger; + private readonly IInfoBarService infoBarService; + private readonly IClipboardProvider clipboardProvider; + private readonly HttpClient httpClient; private readonly TypedEventHandler webMessageReceivedEventHandler; private readonly TypedEventHandler domContentLoadedEventHandler; @@ -106,6 +109,9 @@ internal class MiHoYoJSBridge taskContext = serviceProvider.GetRequiredService(); logger = serviceProvider.GetRequiredService>(); + infoBarService = serviceProvider.GetRequiredService(); + clipboardProvider = serviceProvider.GetRequiredService(); + httpClient = serviceProvider.GetRequiredService(); webMessageReceivedEventHandler = OnWebMessageReceived; domContentLoadedEventHandler = OnDOMContentLoaded; @@ -370,79 +376,47 @@ internal class MiHoYoJSBridge protected virtual async ValueTask Share(JsParam param) { - IInfoBarService infoBarService = serviceProvider.GetRequiredService(); - IClipboardProvider clipboardProvider = serviceProvider.GetRequiredService(); - - string shareType = param.Payload.Type; - ShareContent shareContent = param.Payload.Content; - if (shareType is "image") + if (param.Payload.Type is "image") { - if (shareContent.ImageUrl is not null) + if (param.Payload.Content.ImageUrl is { } imageUrl) { - using (HttpClient httpClient = new()) + using (Stream stream = await httpClient.GetStreamAsync(imageUrl).ConfigureAwait(false)) { - using (Stream stream = await httpClient.GetStreamAsync(shareContent.ImageUrl).ConfigureAwait(false)) + using (InMemoryRandomAccessStream origStream = new()) { - using (InMemoryRandomAccessStream origStream = new()) + await stream.CopyToAsync(origStream.AsStreamForWrite()).ConfigureAwait(false); + using (InMemoryRandomAccessStream imageStream = 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)) { - BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, imageStream); - BitmapDecoder decoder = await BitmapDecoder.CreateAsync(origStream); - encoder.SetSoftwareBitmap(await decoder.GetSoftwareBitmapAsync()); - await encoder.FlushAsync(); - if (clipboardProvider.SetBitmap(imageStream)) - { - infoBarService.Success("复制成功", "图片已复制到剪贴板"); - } - else - { - infoBarService.Error("复制失败", string.Empty); - } + infoBarService.Success(SH.WebBridgeShareCopyToClipboardSuccess); + } + else + { + infoBarService.Error(SH.WebBridgeShareCopyToClipboardFailed); } } } } } - else if (shareContent.ImageBase64 is not null) + else if (param.Payload.Content.ImageBase64 is { } imageBase64) { - using (MemoryStream imageStream = new()) - { - await imageStream.WriteAsync(Convert.FromBase64String(shareContent.ImageBase64)).ConfigureAwait(false); - if (clipboardProvider.SetBitmap(imageStream.AsRandomAccessStream())) - { - infoBarService.Success("复制成功", "图片已复制到剪贴板"); - } - else - { - infoBarService.Error("复制失败", string.Empty); - } - } + await ShareImageBase64CoreAsync(imageBase64).ConfigureAwait(false); } } - else if (shareType is "screenshot") + else if (param.Payload.Type is "screenshot") { - if (shareContent.Preview) - { - 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 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); - using (MemoryStream imageStream = new()) - { - await imageStream.WriteAsync(Convert.FromBase64String(base64)).ConfigureAwait(false); - if (clipboardProvider.SetBitmap(imageStream.AsRandomAccessStream())) - { - infoBarService.Success("复制成功", "图片已复制到剪贴板"); - } - else - { - infoBarService.Error("复制失败", string.Empty); - } - } - } + await ShareImageBase64CoreAsync(base64).ConfigureAwait(false); } return new JsResult>() @@ -454,6 +428,30 @@ 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 ShowAlertDialogAsync(JsParam param) { return ValueTask.FromException(new NotSupportedException()); From d43f2e76c4d93282fac1863166f6c36c265c91a5 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Wed, 3 Jan 2024 14:26:21 +0800 Subject: [PATCH 4/4] code style --- .../Web/Bridge/BridgeShareContext.cs | 41 +++++ .../Web/Bridge/BridgeShareImplmentation.cs | 108 +++++++++++++ .../Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs | 148 ++---------------- .../Web/Bridge/Model/ShareContent.cs | 8 - 4 files changed, 161 insertions(+), 144 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Bridge/BridgeShareContext.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Bridge/BridgeShareImplmentation.cs 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 c900adf6..21307c6b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs @@ -84,15 +84,12 @@ 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; private readonly ITaskContext taskContext; private readonly ILogger logger; - private readonly IInfoBarService infoBarService; - private readonly IClipboardProvider clipboardProvider; - private readonly HttpClient httpClient; private readonly TypedEventHandler webMessageReceivedEventHandler; private readonly TypedEventHandler domContentLoadedEventHandler; @@ -109,9 +106,6 @@ internal class MiHoYoJSBridge taskContext = serviceProvider.GetRequiredService(); logger = serviceProvider.GetRequiredService>(); - infoBarService = serviceProvider.GetRequiredService(); - clipboardProvider = serviceProvider.GetRequiredService(); - httpClient = serviceProvider.GetRequiredService(); webMessageReceivedEventHandler = OnWebMessageReceived; domContentLoadedEventHandler = OnDOMContentLoaded; @@ -132,11 +126,6 @@ internal class MiHoYoJSBridge coreWebView2 = default!; } - /// - /// 关闭 - /// - /// 参数 - /// 响应 protected virtual async ValueTask ClosePageAsync(JsParam param) { await taskContext.SwitchToMainThreadAsync(); @@ -152,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 @@ -175,11 +154,6 @@ internal class MiHoYoJSBridge .ConfigureAwait(false); } - /// - /// 异步获取账户信息 - /// - /// 参数 - /// 响应 protected virtual JsResult> GetCookieInfo(JsParam param) { ArgumentNullException.ThrowIfNull(userAndUid.User.LToken); @@ -195,11 +169,6 @@ internal class MiHoYoJSBridge }; } - /// - /// 获取CookieToken - /// - /// 参数 - /// 响应 protected virtual async ValueTask>> GetCookieTokenAsync(JsParam param) { IUserService userService = serviceProvider.GetRequiredService(); @@ -215,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(); @@ -234,11 +198,6 @@ internal class MiHoYoJSBridge }; } - /// - /// 获取1代动态密钥 - /// - /// 参数 - /// 响应 protected virtual JsResult> GetDynamicSecrectV1(JsParam param) { DataSignOptions options = DataSignOptions.CreateForGeneration1(SaltType.LK2, true); @@ -251,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()); @@ -268,11 +222,6 @@ internal class MiHoYoJSBridge }; } - /// - /// 获取Http请求头 - /// - /// 参数 - /// Http请求头 protected virtual JsResult> GetHttpRequestHeader(JsParam param) { Dictionary headers = new() @@ -306,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 @@ -374,81 +313,18 @@ internal class MiHoYoJSBridge return null; } - protected virtual async ValueTask Share(JsParam param) + protected virtual async ValueTask ShareAsync(JsParam param) { - if (param.Payload.Type is "image") + using (IServiceScope scope = serviceProvider.CreateScope()) { - 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); + JsonSerializerOptions jsonSerializerOptions = scope.ServiceProvider.GetRequiredService(); + HttpClient httpClient = scope.ServiceProvider.GetRequiredService(); + IClipboardProvider clipboardProvider = scope.ServiceProvider.GetRequiredService(); + IInfoBarService infoBarService = scope.ServiceProvider.GetRequiredService(); - await ShareImageBase64CoreAsync(base64).ConfigureAwait(false); - } + BridgeShareContext context = new(coreWebView2, taskContext, httpClient, infoBarService, clipboardProvider, jsonSerializerOptions); - return new JsResult>() - { - Data = new() - { - ["type"] = param.Payload.Type, - }, - }; - } - - 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); - } - } + return await BridgeShareImplmentation.ShareAsync(param, context).ConfigureAwait(false); } } @@ -519,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()) @@ -533,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); @@ -582,7 +458,7 @@ internal class MiHoYoJSBridge "hideLoading" => null, "login" => null, "pushPage" => await PushPageAsync(param).ConfigureAwait(false), - "share" => await Share(param).ConfigureAwait(false), + "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 26437013..efc607f9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/ShareContent.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/Model/ShareContent.cs @@ -6,20 +6,12 @@ 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 }