diff --git a/lib/src/enums.dart b/lib/src/enums.dart index 119f320..76522fa 100644 --- a/lib/src/enums.dart +++ b/lib/src/enums.dart @@ -6,6 +6,12 @@ enum LoadingState { none, loading, navigationCompleted } // Order must match WebviewPointerButton (see webview.h) enum PointerButton { none, primary, secondary, tertiary } +enum WebviewDownloadEventKind { + downloadStarted, + downloadCompleted, + downloadProgress +} + /// Pointer Event kind // Order must match WebviewPointerEventKind (see webview.h) enum WebviewPointerEventKind { activate, down, enter, leave, up, update } @@ -44,23 +50,23 @@ enum WebviewPopupWindowPolicy { allow, deny, sameWindow } enum WebviewHostResourceAccessKind { deny, allow, denyCors } enum WebErrorStatus { - WebErrorStatusUnknown, - WebErrorStatusCertificateCommonNameIsIncorrect, - WebErrorStatusCertificateExpired, - WebErrorStatusClientCertificateContainsErrors, - WebErrorStatusCertificateRevoked, - WebErrorStatusCertificateIsInvalid, - WebErrorStatusServerUnreachable, - WebErrorStatusTimeout, - WebErrorStatusErrorHTTPInvalidServerResponse, - WebErrorStatusConnectionAborted, - WebErrorStatusConnectionReset, - WebErrorStatusDisconnected, - WebErrorStatusCannotConnect, - WebErrorStatusHostNameNotResolved, - WebErrorStatusOperationCanceled, - WebErrorStatusRedirectFailed, - WebErrorStatusUnexpectedError, - WebErrorStatusValidAuthenticationCredentialsRequired, - WebErrorStatusValidProxyAuthenticationRequired, + WebErrorStatusUnknown, + WebErrorStatusCertificateCommonNameIsIncorrect, + WebErrorStatusCertificateExpired, + WebErrorStatusClientCertificateContainsErrors, + WebErrorStatusCertificateRevoked, + WebErrorStatusCertificateIsInvalid, + WebErrorStatusServerUnreachable, + WebErrorStatusTimeout, + WebErrorStatusErrorHTTPInvalidServerResponse, + WebErrorStatusConnectionAborted, + WebErrorStatusConnectionReset, + WebErrorStatusDisconnected, + WebErrorStatusCannotConnect, + WebErrorStatusHostNameNotResolved, + WebErrorStatusOperationCanceled, + WebErrorStatusRedirectFailed, + WebErrorStatusUnexpectedError, + WebErrorStatusValidAuthenticationCredentialsRequired, + WebErrorStatusValidProxyAuthenticationRequired, } diff --git a/lib/src/webview.dart b/lib/src/webview.dart index 4b54810..34328c5 100644 --- a/lib/src/webview.dart +++ b/lib/src/webview.dart @@ -16,6 +16,21 @@ class HistoryChanged { const HistoryChanged(this.canGoBack, this.canGoForward); } +class WebviewDownloadEvent { + final WebviewDownloadEventKind kind; + final String url; + final String resultFilePath; + final int bytesReceived; + final int totalBytesToReceive; + const WebviewDownloadEvent( + this.kind, + this.url, + this.resultFilePath, + this.bytesReceived, + this.totalBytesToReceive, + ); +} + typedef PermissionRequestedDelegate = FutureOr Function( String url, WebviewPermissionKind permissionKind, bool isUserInitiated); @@ -111,12 +126,19 @@ class WebviewController extends ValueNotifier { final StreamController _loadingStateStreamController = StreamController.broadcast(); + + final StreamController _downloadEventStreamController = + StreamController.broadcast(); + final StreamController _onLoadErrorStreamController = StreamController(); /// A stream reflecting the current loading state. Stream get loadingState => _loadingStateStreamController.stream; + Stream get onDownloadEvent => + _downloadEventStreamController.stream; + /// A stream reflecting the navigation error when navigation completed with an error. Stream get onLoadError => _onLoadErrorStreamController.stream; @@ -189,6 +211,16 @@ class WebviewController extends ValueNotifier { final value = LoadingState.values[map['value']]; _loadingStateStreamController.add(value); break; + case 'downloadEvent': + final value = WebviewDownloadEvent( + WebviewDownloadEventKind.values[map['value']['kind']], + map['value']['url'], + map['value']['resultFilePath'], + map['value']['bytesReceived'], + map['value']['totalBytesToReceive'], + ); + _downloadEventStreamController.add(value); + break; case 'historyChanged': final value = HistoryChanged( map['value']['canGoBack'], map['value']['canGoForward']); diff --git a/windows/webview.cc b/windows/webview.cc index 0de207a..4bc9715 100644 --- a/windows/webview.cc +++ b/windows/webview.cc @@ -371,6 +371,52 @@ void Webview::RegisterEventHandlers() { }) .Get(), &event_registrations_.contains_fullscreen_element_changed_token_); + + auto webview24 = webview_.try_query(); + if (webview24) { + webview24->add_DownloadStarting( + Callback( + [this](ICoreWebView2* sender, + ICoreWebView2DownloadStartingEventArgs* args) -> HRESULT { + wil::com_ptr deferral; + args->GetDeferral(&deferral); + + args->put_Handled(TRUE); + + wil::com_ptr download; + args->get_DownloadOperation(&download); + + INT64 totalBytesToReceive = 0; + download->get_TotalBytesToReceive(&totalBytesToReceive); + + wil::unique_cotaskmem_string uri; + download->get_Uri(&uri); + + wil::unique_cotaskmem_string mimeType; + download->get_MimeType(&mimeType); + + wil::unique_cotaskmem_string contentDisposition; + download->get_ContentDisposition(&contentDisposition); + + wil::unique_cotaskmem_string resultFilePath; + args->get_ResultFilePath(&resultFilePath); + + args->put_ResultFilePath(resultFilePath.get()); + UpdateDownloadProgress(download.get()); + + if (download_event_callback_) { + download_event_callback_( + {WebviewDownloadEventKind::DownloadStarted, + util::Utf8FromUtf16(uri.get()), + util::Utf8FromUtf16(resultFilePath.get()), 0, + totalBytesToReceive}); + } + + return S_OK; + }) + .Get(), + &event_registrations_.download_starting_token_); + } } void Webview::SetSurfaceSize(size_t width, size_t height, float scale_factor) { @@ -795,3 +841,66 @@ bool Webview::ClearVirtualHostNameMapping(const std::string& hostName) { return webview->ClearVirtualHostNameToFolderMapping( util::Utf16FromUtf8(hostName).c_str()); } + +void Webview::UpdateDownloadProgress(ICoreWebView2DownloadOperation* download) { + download->add_BytesReceivedChanged( + Callback( + [this](ICoreWebView2DownloadOperation* download, + IUnknown* args) -> HRESULT { + if (download_event_callback_) { + INT64 recvd = 0; + download->get_BytesReceived(&recvd); + INT64 total = 0; + download->get_TotalBytesToReceive(&total); + + wil::unique_cotaskmem_string uri; + download->get_Uri(&uri); + wil::unique_cotaskmem_string resultFilePath; + download->get_ResultFilePath(&resultFilePath); + download_event_callback_( + {WebviewDownloadEventKind::DownloadProgress, + util::Utf8FromUtf16(uri.get()), + util::Utf8FromUtf16(resultFilePath.get()), recvd, total}); + } + return S_OK; + }) + .Get(), + &event_registrations_.download_bytes_received_token_); + + download->add_StateChanged( + Callback( + [this](ICoreWebView2DownloadOperation* download, + IUnknown* args) -> HRESULT { + COREWEBVIEW2_DOWNLOAD_STATE state; + download->get_State(&state); + switch (state) { + case COREWEBVIEW2_DOWNLOAD_STATE_IN_PROGRESS: + break; + case COREWEBVIEW2_DOWNLOAD_STATE_INTERRUPTED: + // Here developer can take different actions based on + // `download->InterruptReason`. For example, show an error + // message to the end user. + break; + case COREWEBVIEW2_DOWNLOAD_STATE_COMPLETED: + if (download_event_callback_) { + wil::unique_cotaskmem_string uri; + download->get_Uri(&uri); + wil::unique_cotaskmem_string resultFilePath; + download->get_ResultFilePath(&resultFilePath); + INT64 recvd = 0; + download->get_BytesReceived(&recvd); + INT64 total = 0; + download->get_TotalBytesToReceive(&total); + download_event_callback_( + {WebviewDownloadEventKind::DownloadCompleted, + util::Utf8FromUtf16(uri.get()), + util::Utf8FromUtf16(resultFilePath.get()), recvd, + total}); + } + break; + } + return S_OK; + }) + .Get(), + &event_registrations_.download_state_changed_token_); +} \ No newline at end of file diff --git a/windows/webview.h b/windows/webview.h index 567e742..9fc3d8b 100644 --- a/windows/webview.h +++ b/windows/webview.h @@ -16,6 +16,12 @@ enum class WebviewPointerButton { None, Primary, Secondary, Tertiary }; enum class WebviewPointerEventKind { Activate, Down, Enter, Leave, Up, Update }; +enum class WebviewDownloadEventKind { + DownloadStarted, + DownloadCompleted, + DownloadProgress +}; + enum class WebviewPermissionKind { Unknown, Microphone, @@ -37,6 +43,14 @@ struct WebviewHistoryChanged { BOOL can_go_forward; }; +struct WebviewDownloadEvent { + WebviewDownloadEventKind kind; + std::string url; + std::string resultFilePath; + INT64 bytesReceived; + INT64 totalBytesToReceive; +}; + struct VirtualKeyState { public: inline void set_isLeftButtonDown(bool is_down) { @@ -87,6 +101,9 @@ struct EventRegistrations { EventRegistrationToken devtools_protocol_event_token_{}; EventRegistrationToken new_windows_requested_token_{}; EventRegistrationToken contains_fullscreen_element_changed_token_{}; + EventRegistrationToken download_starting_token_{}; + EventRegistrationToken download_bytes_received_token_{}; + EventRegistrationToken download_state_changed_token_{}; }; class Webview { @@ -116,6 +133,7 @@ class Webview { PermissionRequestedCallback; typedef std::function ContainsFullScreenElementChangedCallback; + typedef std::function DownloadEventCallback; ~Webview(); @@ -160,6 +178,8 @@ class Webview { WebviewHostResourceAccessKind accessKind); bool ClearVirtualHostNameMapping(const std::string& hostName); + void UpdateDownloadProgress(ICoreWebView2DownloadOperation* download); + void OnUrlChanged(UrlChangedCallback callback) { url_changed_callback_ = std::move(callback); } @@ -172,6 +192,10 @@ class Webview { loading_state_changed_callback_ = std::move(callback); } + void OnDownloadEvent(DownloadEventCallback callback) { + download_event_callback_ = std::move(callback); + } + void OnHistoryChanged(HistoryChangedCallback callback) { history_changed_callback_ = std::move(callback); } @@ -234,6 +258,7 @@ class Webview { UrlChangedCallback url_changed_callback_; LoadingStateChangedCallback loading_state_changed_callback_; + DownloadEventCallback download_event_callback_; OnLoadErrorCallback on_load_error_callback_; HistoryChangedCallback history_changed_callback_; DocumentTitleChangedCallback document_title_changed_callback_; diff --git a/windows/webview_bridge.cc b/windows/webview_bridge.cc index 6ea5b7d..f0cae1d 100644 --- a/windows/webview_bridge.cc +++ b/windows/webview_bridge.cc @@ -228,6 +228,28 @@ void WebviewBridge::RegisterEventHandlers() { EmitEvent(event); }); + webview_->OnDownloadEvent([this](WebviewDownloadEvent webviewDownloadEvent) { + const auto event = flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue(kEventType), + flutter::EncodableValue("downloadEvent")}, + {flutter::EncodableValue(kEventValue), + flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue("kind"), + flutter::EncodableValue( + static_cast(webviewDownloadEvent.kind))}, + {flutter::EncodableValue("url"), + flutter::EncodableValue(webviewDownloadEvent.url)}, + {flutter::EncodableValue("resultFilePath"), + flutter::EncodableValue(webviewDownloadEvent.resultFilePath)}, + {flutter::EncodableValue("bytesReceived"), + flutter::EncodableValue(webviewDownloadEvent.bytesReceived)}, + {flutter::EncodableValue("totalBytesToReceive"), + flutter::EncodableValue( + webviewDownloadEvent.totalBytesToReceive)}, + })}}); + EmitEvent(event); + }); + webview_->OnHistoryChanged([this](WebviewHistoryChanged historyChanged) { const auto event = flutter::EncodableValue(flutter::EncodableMap{ {flutter::EncodableValue(kEventType),