From f624f7c3374c81f8d2191b51a3df8339c759c309 Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Mon, 17 Oct 2022 02:23:05 +0200 Subject: [PATCH 1/9] initial macos implementation --- .metadata | 33 +- .../lib/in_app_browser_example.screen.dart | 40 +- example/lib/in_app_webiew_example.screen.dart | 2 +- example/lib/main.dart | 72 +- ...authentication_session_example.screen.dart | 3 +- example/macos/.gitignore | 7 + example/macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 16 + example/macos/Podfile | 40 + .../macos/Runner.xcodeproj/project.pbxproj | 632 +++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + example/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 + .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes example/macos/Runner/Base.lproj/MainMenu.xib | 343 +++ example/macos/Runner/Configs/AppInfo.xcconfig | 14 + example/macos/Runner/Configs/Debug.xcconfig | 2 + example/macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 18 + example/macos/Runner/Info.plist | 32 + example/macos/Runner/MainFlutterWindow.swift | 15 + example/macos/Runner/Release.entitlements | 16 + .../InAppBrowser/InAppBrowserManager.swift | 2 +- .../InAppBrowser/InAppBrowserSettings.swift | 2 +- .../InAppBrowserWebViewController.swift | 2 +- lib/src/in_app_webview/in_app_webview.dart | 2 +- lib/src/types/print_job_color_mode.dart | 7 + lib/src/types/print_job_color_mode.g.dart | 40 +- lib/src/types/print_job_duplex_mode.dart | 6 +- lib/src/types/print_job_duplex_mode.g.dart | 9 + lib/src/types/print_job_orientation.dart | 17 + lib/src/types/print_job_orientation.g.dart | 32 +- lib/src/types/ssl_certificate.dart | 10 +- lib/src/types/ssl_certificate.g.dart | 10 +- macos/Classes/CredentialDatabase.swift | 201 ++ .../HeadlessInAppWebView.swift | 68 + .../HeadlessInAppWebViewManager.swift | 68 + .../HeadlessWebViewChannelDelegate.swift | 64 + macos/Classes/ISettings.swift | 50 + .../InAppBrowserChannelDelegate.swift | 29 + .../InAppBrowser/InAppBrowserDelegate.swift | 17 + .../InAppBrowser/InAppBrowserManager.swift | 107 + .../InAppBrowser/InAppBrowserSettings.swift | 45 + .../InAppBrowserWebViewController.swift | 302 ++ .../InAppBrowser/InAppBrowserWindow.swift | 276 ++ .../InAppWebView/ContextMenuSettings.swift | 17 + .../InAppWebView/CustomSchemeHandler.swift | 45 + .../FlutterWebViewController.swift | 179 ++ .../InAppWebView/FlutterWebViewFactory.swift | 35 + macos/Classes/InAppWebView/InAppWebView.swift | 2485 +++++++++++++++++ .../InAppWebView/InAppWebViewSettings.swift | 136 + .../WebMessage/WebMessageChannel.swift | 75 + .../WebMessageChannelChannelDelegate.swift | 105 + .../WebMessage/WebMessageListener.swift | 184 ++ .../WebMessageListenerChannelDelegate.swift | 72 + .../InAppWebView/WebViewChannelDelegate.swift | 1085 +++++++ .../WebViewChannelDelegateMethods.swift | 84 + macos/Classes/InAppWebViewFlutterPlugin.swift | 8 + macos/Classes/InAppWebViewStatic.swift | 81 + macos/Classes/LeakAvoider.swift | 26 + macos/Classes/MyCookieManager.swift | 309 ++ macos/Classes/MyWebStorageManager.swift | 112 + macos/Classes/PlatformUtil.swift | 66 + ...llAsyncJavaScriptBelowIOS14WrapperJS.swift | 21 + .../PluginScriptsJS/ConsoleLogJS.swift | 58 + .../EnableViewportScaleJS.swift | 36 + .../FindElementsAtPointJS.swift | 73 + .../PluginScriptsJS/FindTextHighlightJS.swift | 187 ++ .../InterceptAjaxRequestJS.swift | 251 ++ .../InterceptFetchRequestJS.swift | 153 + .../PluginScriptsJS/JavaScriptBridgeJS.swift | 243 ++ .../LastTouchedAnchorOrImageJS.swift | 62 + .../PluginScriptsJS/OnLoadResourceJS.swift | 39 + .../PluginScriptsJS/OnScrollChangedJS.swift | 33 + .../PluginScriptsJS/OnWindowBlurEventJS.swift | 26 + .../OnWindowFocusEventJS.swift | 26 + .../OriginalViewPortMetaTagContentJS.swift | 31 + .../PluginScriptsJS/PluginScriptsUtil.swift | 31 + macos/Classes/PluginScriptsJS/PrintJS.swift | 24 + .../PluginScriptsJS/PromisePolyfillJS.swift | 27 + .../PluginScriptsJS/SupportZoomJS.swift | 37 + .../PluginScriptsJS/WebMessageChannelJS.swift | 10 + .../WebMessageListenerJS.swift | 112 + .../Classes/PluginScriptsJS/WindowIdJS.swift | 19 + .../PrintJob/CustomUIPrintPageRenderer.swift | 34 + macos/Classes/PrintJob/PrintAttributes.swift | 42 + .../PrintJob/PrintJobChannelDelegate.swift | 60 + .../Classes/PrintJob/PrintJobController.swift | 88 + macos/Classes/PrintJob/PrintJobInfo.swift | 53 + macos/Classes/PrintJob/PrintJobManager.swift | 28 + macos/Classes/PrintJob/PrintJobSettings.swift | 154 + macos/Classes/SwiftFlutterPlugin.swift | 86 + macos/Classes/Types/BaseCallbackResult.swift | 32 + macos/Classes/Types/CGRect.swift | 23 + macos/Classes/Types/CallbackResult.swift | 18 + macos/Classes/Types/ChannelDelegate.swift | 28 + macos/Classes/Types/ClientCertChallenge.swift | 22 + macos/Classes/Types/ClientCertResponse.swift | 33 + macos/Classes/Types/CreateWindowAction.swift | 31 + .../Classes/Types/CustomSchemeResponse.swift | 31 + macos/Classes/Types/Disposable.swift | 12 + .../Classes/Types/DownloadStartRequest.swift | 42 + macos/Classes/Types/FindSession.swift | 28 + .../Types/FlutterMethodCallDelegate.swift | 19 + .../Classes/Types/FlutterMethodChannel.swift | 26 + macos/Classes/Types/HitTestResult.swift | 44 + macos/Classes/Types/HttpAuthResponse.swift | 33 + .../Types/HttpAuthenticationChallenge.swift | 34 + macos/Classes/Types/JsAlertResponse.swift | 33 + macos/Classes/Types/JsConfirmResponse.swift | 36 + macos/Classes/Types/JsPromptResponse.swift | 43 + macos/Classes/Types/MethodChannelResult.swift | 14 + macos/Classes/Types/NSAttributedString.swift | 63 + macos/Classes/Types/NSColor.swift | 57 + macos/Classes/Types/NSEdgeInsets.swift | 23 + macos/Classes/Types/PermissionRequest.swift | 29 + macos/Classes/Types/PermissionResponse.swift | 27 + macos/Classes/Types/PluginScript.swift | 90 + macos/Classes/Types/SecCertificate.swift | 18 + .../Types/ServerTrustAuthResponse.swift | 24 + .../Classes/Types/ServerTrustChallenge.swift | 22 + macos/Classes/Types/Size2D.swift | 35 + macos/Classes/Types/SslCertificate.swift | 30 + macos/Classes/Types/SslError.swift | 50 + macos/Classes/Types/StringOrInt.swift | 13 + .../Types/URLAuthenticationChallenge.swift | 20 + macos/Classes/Types/URLCredential.swift | 26 + macos/Classes/Types/URLProtectionSpace.swift | 63 + macos/Classes/Types/URLRequest.swift | 99 + macos/Classes/Types/URLResponse.swift | 31 + macos/Classes/Types/UserScript.swift | 71 + macos/Classes/Types/WKContentWorld.swift | 41 + macos/Classes/Types/WKFrameInfo.swift | 26 + macos/Classes/Types/WKNavigationAction.swift | 28 + .../Classes/Types/WKNavigationResponse.swift | 19 + macos/Classes/Types/WKSecurityOrigin.swift | 20 + .../Types/WKUserContentController.swift | 352 +++ macos/Classes/Types/WKWindowFeatures.swift | 24 + macos/Classes/Types/WebMessage.swift | 28 + macos/Classes/Types/WebMessagePort.swift | 123 + macos/Classes/Types/WebResourceError.swift | 25 + macos/Classes/Types/WebResourceRequest.swift | 53 + macos/Classes/Types/WebResourceResponse.swift | 47 + macos/Classes/Types/WebViewTransport.swift | 18 + macos/Classes/Util.swift | 140 + macos/Classes/WKProcessPoolManager.swift | 13 + .../WebAuthenticationSession.swift | 99 + ...AuthenticationSessionChannelDelegate.swift | 74 + .../WebAuthenticationSessionManager.swift | 78 + .../WebAuthenticationSessionSettings.swift | 28 + macos/flutter_inappwebview.podspec | 27 + pubspec.yaml | 4 +- 163 files changed, 12590 insertions(+), 58 deletions(-) create mode 100644 example/macos/.gitignore create mode 100644 example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 example/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 example/macos/Podfile create mode 100644 example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/macos/Runner/AppDelegate.swift create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 example/macos/Runner/Configs/Debug.xcconfig create mode 100644 example/macos/Runner/Configs/Release.xcconfig create mode 100644 example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 example/macos/Runner/DebugProfile.entitlements create mode 100644 example/macos/Runner/Info.plist create mode 100644 example/macos/Runner/MainFlutterWindow.swift create mode 100644 example/macos/Runner/Release.entitlements create mode 100755 macos/Classes/CredentialDatabase.swift create mode 100644 macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift create mode 100644 macos/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift create mode 100644 macos/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift create mode 100755 macos/Classes/ISettings.swift create mode 100644 macos/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift create mode 100644 macos/Classes/InAppBrowser/InAppBrowserDelegate.swift create mode 100755 macos/Classes/InAppBrowser/InAppBrowserManager.swift create mode 100755 macos/Classes/InAppBrowser/InAppBrowserSettings.swift create mode 100755 macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift create mode 100644 macos/Classes/InAppBrowser/InAppBrowserWindow.swift create mode 100644 macos/Classes/InAppWebView/ContextMenuSettings.swift create mode 100755 macos/Classes/InAppWebView/CustomSchemeHandler.swift create mode 100755 macos/Classes/InAppWebView/FlutterWebViewController.swift create mode 100755 macos/Classes/InAppWebView/FlutterWebViewFactory.swift create mode 100755 macos/Classes/InAppWebView/InAppWebView.swift create mode 100755 macos/Classes/InAppWebView/InAppWebViewSettings.swift create mode 100644 macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift create mode 100644 macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift create mode 100644 macos/Classes/InAppWebView/WebMessage/WebMessageListener.swift create mode 100644 macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift create mode 100644 macos/Classes/InAppWebView/WebViewChannelDelegate.swift create mode 100644 macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift create mode 100644 macos/Classes/InAppWebViewFlutterPlugin.swift create mode 100755 macos/Classes/InAppWebViewStatic.swift create mode 100755 macos/Classes/LeakAvoider.swift create mode 100755 macos/Classes/MyCookieManager.swift create mode 100755 macos/Classes/MyWebStorageManager.swift create mode 100644 macos/Classes/PlatformUtil.swift create mode 100644 macos/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift create mode 100644 macos/Classes/PluginScriptsJS/ConsoleLogJS.swift create mode 100644 macos/Classes/PluginScriptsJS/EnableViewportScaleJS.swift create mode 100644 macos/Classes/PluginScriptsJS/FindElementsAtPointJS.swift create mode 100644 macos/Classes/PluginScriptsJS/FindTextHighlightJS.swift create mode 100644 macos/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift create mode 100644 macos/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift create mode 100644 macos/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift create mode 100644 macos/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift create mode 100644 macos/Classes/PluginScriptsJS/OnLoadResourceJS.swift create mode 100644 macos/Classes/PluginScriptsJS/OnScrollChangedJS.swift create mode 100644 macos/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift create mode 100644 macos/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift create mode 100644 macos/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift create mode 100644 macos/Classes/PluginScriptsJS/PluginScriptsUtil.swift create mode 100644 macos/Classes/PluginScriptsJS/PrintJS.swift create mode 100644 macos/Classes/PluginScriptsJS/PromisePolyfillJS.swift create mode 100644 macos/Classes/PluginScriptsJS/SupportZoomJS.swift create mode 100644 macos/Classes/PluginScriptsJS/WebMessageChannelJS.swift create mode 100644 macos/Classes/PluginScriptsJS/WebMessageListenerJS.swift create mode 100644 macos/Classes/PluginScriptsJS/WindowIdJS.swift create mode 100644 macos/Classes/PrintJob/CustomUIPrintPageRenderer.swift create mode 100644 macos/Classes/PrintJob/PrintAttributes.swift create mode 100644 macos/Classes/PrintJob/PrintJobChannelDelegate.swift create mode 100644 macos/Classes/PrintJob/PrintJobController.swift create mode 100644 macos/Classes/PrintJob/PrintJobInfo.swift create mode 100644 macos/Classes/PrintJob/PrintJobManager.swift create mode 100644 macos/Classes/PrintJob/PrintJobSettings.swift create mode 100755 macos/Classes/SwiftFlutterPlugin.swift create mode 100644 macos/Classes/Types/BaseCallbackResult.swift create mode 100644 macos/Classes/Types/CGRect.swift create mode 100644 macos/Classes/Types/CallbackResult.swift create mode 100644 macos/Classes/Types/ChannelDelegate.swift create mode 100644 macos/Classes/Types/ClientCertChallenge.swift create mode 100644 macos/Classes/Types/ClientCertResponse.swift create mode 100644 macos/Classes/Types/CreateWindowAction.swift create mode 100644 macos/Classes/Types/CustomSchemeResponse.swift create mode 100644 macos/Classes/Types/Disposable.swift create mode 100644 macos/Classes/Types/DownloadStartRequest.swift create mode 100644 macos/Classes/Types/FindSession.swift create mode 100755 macos/Classes/Types/FlutterMethodCallDelegate.swift create mode 100644 macos/Classes/Types/FlutterMethodChannel.swift create mode 100644 macos/Classes/Types/HitTestResult.swift create mode 100644 macos/Classes/Types/HttpAuthResponse.swift create mode 100644 macos/Classes/Types/HttpAuthenticationChallenge.swift create mode 100644 macos/Classes/Types/JsAlertResponse.swift create mode 100644 macos/Classes/Types/JsConfirmResponse.swift create mode 100644 macos/Classes/Types/JsPromptResponse.swift create mode 100644 macos/Classes/Types/MethodChannelResult.swift create mode 100644 macos/Classes/Types/NSAttributedString.swift create mode 100644 macos/Classes/Types/NSColor.swift create mode 100644 macos/Classes/Types/NSEdgeInsets.swift create mode 100644 macos/Classes/Types/PermissionRequest.swift create mode 100644 macos/Classes/Types/PermissionResponse.swift create mode 100644 macos/Classes/Types/PluginScript.swift create mode 100644 macos/Classes/Types/SecCertificate.swift create mode 100644 macos/Classes/Types/ServerTrustAuthResponse.swift create mode 100644 macos/Classes/Types/ServerTrustChallenge.swift create mode 100644 macos/Classes/Types/Size2D.swift create mode 100644 macos/Classes/Types/SslCertificate.swift create mode 100644 macos/Classes/Types/SslError.swift create mode 100644 macos/Classes/Types/StringOrInt.swift create mode 100644 macos/Classes/Types/URLAuthenticationChallenge.swift create mode 100644 macos/Classes/Types/URLCredential.swift create mode 100644 macos/Classes/Types/URLProtectionSpace.swift create mode 100644 macos/Classes/Types/URLRequest.swift create mode 100644 macos/Classes/Types/URLResponse.swift create mode 100644 macos/Classes/Types/UserScript.swift create mode 100644 macos/Classes/Types/WKContentWorld.swift create mode 100644 macos/Classes/Types/WKFrameInfo.swift create mode 100644 macos/Classes/Types/WKNavigationAction.swift create mode 100644 macos/Classes/Types/WKNavigationResponse.swift create mode 100644 macos/Classes/Types/WKSecurityOrigin.swift create mode 100644 macos/Classes/Types/WKUserContentController.swift create mode 100644 macos/Classes/Types/WKWindowFeatures.swift create mode 100644 macos/Classes/Types/WebMessage.swift create mode 100644 macos/Classes/Types/WebMessagePort.swift create mode 100644 macos/Classes/Types/WebResourceError.swift create mode 100644 macos/Classes/Types/WebResourceRequest.swift create mode 100644 macos/Classes/Types/WebResourceResponse.swift create mode 100644 macos/Classes/Types/WebViewTransport.swift create mode 100644 macos/Classes/Util.swift create mode 100755 macos/Classes/WKProcessPoolManager.swift create mode 100644 macos/Classes/WebAuthenticationSession/WebAuthenticationSession.swift create mode 100644 macos/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift create mode 100644 macos/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift create mode 100644 macos/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift create mode 100644 macos/flutter_inappwebview.podspec diff --git a/.metadata b/.metadata index 14879c61..b0199f3a 100644 --- a/.metadata +++ b/.metadata @@ -1,10 +1,39 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: c860cba910319332564e1e9d470a17074c1f2dfd + revision: 18a827f3933c19f51862dde3fa472197683249d6 channel: stable project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 18a827f3933c19f51862dde3fa472197683249d6 + base_revision: 18a827f3933c19f51862dde3fa472197683249d6 + - platform: android + create_revision: 18a827f3933c19f51862dde3fa472197683249d6 + base_revision: 18a827f3933c19f51862dde3fa472197683249d6 + - platform: ios + create_revision: 18a827f3933c19f51862dde3fa472197683249d6 + base_revision: 18a827f3933c19f51862dde3fa472197683249d6 + - platform: macos + create_revision: 18a827f3933c19f51862dde3fa472197683249d6 + base_revision: 18a827f3933c19f51862dde3fa472197683249d6 + - platform: web + create_revision: 18a827f3933c19f51862dde3fa472197683249d6 + base_revision: 18a827f3933c19f51862dde3fa472197683249d6 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/lib/in_app_browser_example.screen.dart b/example/lib/in_app_browser_example.screen.dart index 37e87165..33c3a3a5 100755 --- a/example/lib/in_app_browser_example.screen.dart +++ b/example/lib/in_app_browser_example.screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -18,9 +19,7 @@ class MyInAppBrowser extends InAppBrowser { } @override - Future onLoadStart(url) async { - - } + Future onLoadStart(url) async {} @override Future onLoadStop(url) async { @@ -61,26 +60,30 @@ class InAppBrowserExampleScreen extends StatefulWidget { } class _InAppBrowserExampleScreenState extends State { - late PullToRefreshController pullToRefreshController; + PullToRefreshController? pullToRefreshController; @override void initState() { super.initState(); - pullToRefreshController = PullToRefreshController( - settings: PullToRefreshSettings( - color: Colors.black, - ), - onRefresh: () async { - if (Platform.isAndroid) { - widget.browser.webViewController.reload(); - } else if (Platform.isIOS) { - widget.browser.webViewController.loadUrl( - urlRequest: URLRequest( - url: await widget.browser.webViewController.getUrl())); - } - }, - ); + pullToRefreshController = kIsWeb || + ![TargetPlatform.iOS, TargetPlatform.android] + .contains(defaultTargetPlatform) + ? null + : PullToRefreshController( + settings: PullToRefreshSettings( + color: Colors.black, + ), + onRefresh: () async { + if (Platform.isAndroid) { + widget.browser.webViewController.reload(); + } else if (Platform.isIOS) { + widget.browser.webViewController.loadUrl( + urlRequest: URLRequest( + url: await widget.browser.webViewController.getUrl())); + } + }, + ); widget.browser.pullToRefreshController = pullToRefreshController; } @@ -103,6 +106,7 @@ class _InAppBrowserExampleScreenState extends State { URLRequest(url: Uri.parse("https://flutter.dev")), settings: InAppBrowserClassSettings( browserSettings: InAppBrowserSettings( + hidden: false, toolbarTopBackgroundColor: Colors.blue, presentationStyle: ModalPresentationStyle.POPOVER ), diff --git a/example/lib/in_app_webiew_example.screen.dart b/example/lib/in_app_webiew_example.screen.dart index 10586b29..ee37d50c 100755 --- a/example/lib/in_app_webiew_example.screen.dart +++ b/example/lib/in_app_webiew_example.screen.dart @@ -63,7 +63,7 @@ class _InAppWebViewExampleScreenState extends State { contextMenuItemClicked.title); }); - pullToRefreshController = kIsWeb + pullToRefreshController = kIsWeb || ![TargetPlatform.iOS, TargetPlatform.android].contains(defaultTargetPlatform) ? null : PullToRefreshController( settings: PullToRefreshSettings( diff --git a/example/lib/main.dart b/example/lib/main.dart index 8c7a1612..24450208 100755 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -33,26 +33,65 @@ Future main() async { } PointerInterceptor myDrawer({required BuildContext context}) { - final children = [ + var children = [ ListTile( title: Text('InAppWebView'), onTap: () { Navigator.pushReplacementNamed(context, '/'); }, - ) + ), + ListTile( + title: Text('InAppBrowser'), + onTap: () { + Navigator.pushReplacementNamed(context, '/InAppBrowser'); + }, + ), + ListTile( + title: Text('ChromeSafariBrowser'), + onTap: () { + Navigator.pushReplacementNamed(context, '/ChromeSafariBrowser'); + }, + ), + ListTile( + title: Text('WebAuthenticationSession'), + onTap: () { + Navigator.pushReplacementNamed(context, '/WebAuthenticationSession'); + }, + ), + ListTile( + title: Text('HeadlessInAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/HeadlessInAppWebView'); + }, + ), ]; - if (!kIsWeb) { - children.addAll([ + if (kIsWeb) { + children = [ + ListTile( + title: Text('InAppWebView'), + onTap: () { + Navigator.pushReplacementNamed(context, '/'); + }, + ) + ]; + } else if (defaultTargetPlatform == TargetPlatform.macOS) { + children = [ + // ListTile( + // title: Text('InAppWebView'), + // onTap: () { + // Navigator.pushReplacementNamed(context, '/'); + // }, + // ), + // ListTile( + // title: Text('InAppBrowser'), + // onTap: () { + // Navigator.pushReplacementNamed(context, '/InAppBrowser'); + // }, + // ), ListTile( title: Text('InAppBrowser'), onTap: () { - Navigator.pushReplacementNamed(context, '/InAppBrowser'); - }, - ), - ListTile( - title: Text('ChromeSafariBrowser'), - onTap: () { - Navigator.pushReplacementNamed(context, '/ChromeSafariBrowser'); + Navigator.pushReplacementNamed(context, '/'); }, ), ListTile( @@ -67,7 +106,7 @@ PointerInterceptor myDrawer({required BuildContext context}) { Navigator.pushReplacementNamed(context, '/HeadlessInAppWebView'); }, ), - ]); + ]; } return PointerInterceptor( child: Drawer( @@ -110,6 +149,15 @@ class _MyAppState extends State { '/': (context) => InAppWebViewExampleScreen(), }); } + if (defaultTargetPlatform == TargetPlatform.macOS) { + return MaterialApp(initialRoute: '/', routes: { + // '/': (context) => InAppWebViewExampleScreen(), + // '/InAppBrowser': (context) => InAppBrowserExampleScreen(), + '/': (context) => InAppBrowserExampleScreen(), + '/HeadlessInAppWebView': (context) => HeadlessInAppWebViewExampleScreen(), + '/WebAuthenticationSession': (context) => WebAuthenticationSessionExampleScreen(), + }); + } return MaterialApp(initialRoute: '/', routes: { '/': (context) => InAppWebViewExampleScreen(), '/InAppBrowser': (context) => InAppBrowserExampleScreen(), diff --git a/example/lib/web_authentication_session_example.screen.dart b/example/lib/web_authentication_session_example.screen.dart index aaaec20b..541e5cc1 100755 --- a/example/lib/web_authentication_session_example.screen.dart +++ b/example/lib/web_authentication_session_example.screen.dart @@ -48,7 +48,8 @@ class _WebAuthenticationSessionExampleScreenState onPressed: () async { if (session == null && !kIsWeb && - defaultTargetPlatform == TargetPlatform.iOS && + [TargetPlatform.iOS, TargetPlatform.macOS] + .contains(defaultTargetPlatform) && await WebAuthenticationSession.isAvailable()) { session = await WebAuthenticationSession.create( url: Uri.parse( diff --git a/example/macos/.gitignore b/example/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..5d8a06dd --- /dev/null +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,16 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import flutter_inappwebview +import path_provider_macos +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 00000000..dade8dfa --- /dev/null +++ b/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..f9c8aaaf --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 635B80423A5381B66E47F216 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3C8BEF4D338BF1EDD832305 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* flutter_inappwebview_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_inappwebview_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7D4269DF6E938B573DA3AB1B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A3C8BEF4D338BF1EDD832305 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AC8274DF424C630859617712 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + B44881B5FC807BDF77BD81E9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 635B80423A5381B66E47F216 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + DE8EF0F1212CA8BCD731BA0F /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* flutter_inappwebview_example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A3C8BEF4D338BF1EDD832305 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + DE8EF0F1212CA8BCD731BA0F /* Pods */ = { + isa = PBXGroup; + children = ( + AC8274DF424C630859617712 /* Pods-Runner.debug.xcconfig */, + 7D4269DF6E938B573DA3AB1B /* Pods-Runner.release.xcconfig */, + B44881B5FC807BDF77BD81E9 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 2233BC408EAD04A8B5721F92 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 7E9999C83038C5D12ED26B20 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* flutter_inappwebview_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2233BC408EAD04A8B5721F92 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 7E9999C83038C5D12ED26B20 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..958f17d6 --- /dev/null +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#jR^;j87-Auq zoUlN^K{r-Q+XN;zI ze|?*NFmgt#V#GwrSWaz^2G&@SBmck6ZcIFMww~vE<1E?M2#KUn1CzsB6D2+0SuRV@ zV2kK5HvIGB{HX-hQzs0*AB%5$9RJ@a;)Ahq#p$GSP91^&hi#6sg*;a~dt}4AclK>h z_3MoPRQ{i;==;*1S-mY<(JFzhAxMI&<61&m$J0NDHdJ3tYx~j0%M-uN6Zl8~_0DOkGXc0001@sz3l12C6Xg{AT~( zm6w64BA|AX`Ve)YY-glyudNN>MAfkXz-T7`_`fEolM;0T0BA)(02-OaW z0*cW7Z~ec94o8&g0D$N>b!COu{=m}^%oXZ4?T8ZyPZuGGBPBA7pbQMoV5HYhiT?%! zcae~`(QAN4&}-=#2f5fkn!SWGWmSeCISBcS=1-U|MEoKq=k?_x3apK>9((R zuu$9X?^8?@(a{qMS%J8SJPq))v}Q-ZyDm6Gbie0m92=`YlwnQPQP1kGSm(N2UJ3P6 z^{p-u)SSCTW~c1rw;cM)-uL2{->wCn2{#%;AtCQ!m%AakVs1K#v@(*-6QavyY&v&*wO_rCJXJuq$c$7ZjsW+pJo-$L^@!7X04CvaOpPyfw|FKvu;e(&Iw>Tbg zL}#8e^?X%TReXTt>gsBByt0kSU20oQx*~P=4`&tcZ7N6t-6LiK{LxX*p6}9c<0Pu^ zLx1w_P4P2V>bX=`F%v$#{sUDdF|;rbI{p#ZW`00Bgh(eB(nOIhy8W9T>3aQ=k8Z9% zB+TusFABF~J?N~fAd}1Rme=@4+1=M{^P`~se7}e3;mY0!%#MJf!XSrUC{0uZqMAd7%q zQY#$A>q}noIB4g54Ue)x>ofVm3DKBbUmS4Z-bm7KdKsUixva)1*&z5rgAG2gxG+_x zqT-KNY4g7eM!?>==;uD9Y4iI(Hu$pl8!LrK_Zb}5nv(XKW{9R144E!cFf36p{i|8pRL~p`_^iNo z{mf7y`#hejw#^#7oKPlN_Td{psNpNnM?{7{R-ICBtYxk>?3}OTH_8WkfaTLw)ZRTfxjW+0>gMe zpKg~`Bc$Y>^VX;ks^J0oKhB#6Ukt{oQhN+o2FKGZx}~j`cQB%vVsMFnm~R_1Y&Ml? zwFfb~d|dW~UktY@?zkau>Owe zRroi(<)c4Ux&wJfY=3I=vg)uh;sL(IYY9r$WK1$F;jYqq1>xT{LCkIMb3t2jN8d`9 z=4(v-z7vHucc_fjkpS}mGC{ND+J-hc_0Ix4kT^~{-2n|;Jmn|Xf9wGudDk7bi*?^+ z7fku8z*mbkGm&xf&lmu#=b5mp{X(AwtLTf!N`7FmOmX=4xwbD=fEo8CaB1d1=$|)+ z+Dlf^GzGOdlqTO8EwO?8;r+b;gkaF^$;+#~2_YYVH!hD6r;PaWdm#V=BJ1gH9ZK_9 zrAiIC-)z)hRq6i5+$JVmR!m4P>3yJ%lH)O&wtCyum3A*})*fHODD2nq!1@M>t@Za+ zH6{(Vf>_7!I-APmpsGLYpl7jww@s5hHOj5LCQXh)YAp+y{gG(0UMm(Ur z3o3n36oFwCkn+H*GZ-c6$Y!5r3z*@z0`NrB2C^q#LkOuooUM8Oek2KBk}o1PU8&2L z4iNkb5CqJWs58aR394iCU^ImDqV;q_Pp?pl=RB2372(Io^GA^+oKguO1(x$0<7w3z z)j{vnqEB679Rz4i4t;8|&Zg77UrklxY9@GDq(ZphH6=sW`;@uIt5B?7Oi?A0-BL}(#1&R;>2aFdq+E{jsvpNHjLx2t{@g1}c~DQcPNmVmy| zNMO@ewD^+T!|!DCOf}s9dLJU}(KZy@Jc&2Nq3^;vHTs}Hgcp`cw&gd7#N}nAFe3cM1TF%vKbKSffd&~FG9y$gLyr{#to)nxz5cCASEzQ}gz8O)phtHuKOW6p z@EQF(R>j%~P63Wfosrz8p(F=D|Mff~chUGn(<=CQbSiZ{t!e zeDU-pPsLgtc#d`3PYr$i*AaT!zF#23htIG&?QfcUk+@k$LZI}v+js|yuGmE!PvAV3 ztzh90rK-0L6P}s?1QH`Ot@ilbgMBzWIs zIs6K<_NL$O4lwR%zH4oJ+}JJp-bL6~%k&p)NGDMNZX7)0kni&%^sH|T?A)`z z=adV?!qnWx^B$|LD3BaA(G=ePL1+}8iu^SnnD;VE1@VLHMVdSN9$d)R(Wk{JEOp(P zm3LtAL$b^*JsQ0W&eLaoYag~=fRRdI>#FaELCO7L>zXe6w*nxN$Iy*Q*ftHUX0+N- zU>{D_;RRVPbQ?U+$^%{lhOMKyE5>$?U1aEPist+r)b47_LehJGTu>TcgZe&J{ z{q&D{^Ps~z7|zj~rpoh2I_{gAYNoCIJmio3B}$!5vTF*h$Q*vFj~qbo%bJCCRy509 zHTdDh_HYH8Zb9`}D5;;J9fkWOQi%Y$B1!b9+ESj+B@dtAztlY2O3NE<6HFiqOF&p_ zW-K`KiY@RPSY-p9Q99}Hcd05DT79_pfb{BV7r~?9pWh=;mcKBLTen%THFPo2NN~Nf zriOtFnqx}rtO|A6k!r6 zf-z?y-UD{dT0kT9FJ`-oWuPHbo+3wBS(}?2ql(+e@VTExmfnB*liCb zmeI+v5*+W_L;&kQN^ChW{jE0Mw#0Tfs}`9bk3&7UjxP^Ke(%eJu2{VnW?tu7Iqecm zB5|=-QdzK$=h50~{X3*w4%o1FS_u(dG2s&427$lJ?6bkLet}yYXCy)u_Io1&g^c#( z-$yYmSpxz{>BL;~c+~sxJIe1$7eZI_9t`eB^Pr0)5CuA}w;;7#RvPq|H6!byRzIJG ziQ7a4y_vhj(AL`8PhIm9edCv|%TX#f50lt8+&V+D4<}IA@S@#f4xId80oH$!_!q?@ zFRGGg2mTv&@76P7aTI{)Hu%>3QS_d)pQ%g8BYi58K~m-Ov^7r8BhX7YC1D3vwz&N8{?H*_U7DI?CI)+et?q|eGu>42NJ?K4SY zD?kc>h@%4IqNYuQ8m10+8xr2HYg2qFNdJl=Tmp&ybF>1>pqVfa%SsV*BY$d6<@iJA ziyvKnZ(~F9xQNokBgMci#pnZ}Igh0@S~cYcU_2Jfuf|d3tuH?ZSSYBfM(Y3-JBsC|S9c;# zyIMkPxgrq};0T09pjj#X?W^TFCMf1-9P{)g88;NDI+S4DXe>7d3Mb~i-h&S|Jy{J< zq3736$bH?@{!amD!1Ys-X)9V=#Z={fzsjVYMX5BG6%}tkzwC#1nQLj1y1f#}8**4Y zAvDZHw8)N)8~oWC88CgzbwOrL9HFbk4}h85^ptuu7A+uc#$f^9`EWv1Vr{5+@~@Uv z#B<;-nt;)!k|fRIg;2DZ(A2M2aC65kOIov|?Mhi1Sl7YOU4c$T(DoRQIGY`ycfkn% zViHzL;E*A{`&L?GP06Foa38+QNGA zw3+Wqs(@q+H{XLJbwZzE(omw%9~LPZfYB|NF5%j%E5kr_xE0u;i?IOIchn~VjeDZ) zAqsqhP0vu2&Tbz3IgJvMpKbThC-@=nk)!|?MIPP>MggZg{cUcKsP8|N#cG5 zUXMXxcXBF9`p>09IR?x$Ry3;q@x*%}G#lnB1}r#!WL88I@uvm}X98cZ8KO&cqT1p> z+gT=IxPsq%n4GWgh-Bk8E4!~`r@t>DaQKsjDqYc&h$p~TCh8_Mck5UB84u6Jl@kUZCU9BA-S!*bf>ZotFX9?a_^y%)yH~rsAz0M5#^Di80_tgoKw(egN z`)#(MqAI&A84J#Z<|4`Co8`iY+Cv&iboMJ^f9ROUK0Lm$;-T*c;TCTED_0|qfhlcS zv;BD*$Zko#nWPL}2K8T-?4}p{u)4xon!v_(yVW8VMpxg4Kh^J6WM{IlD{s?%XRT8P|yCU`R&6gwB~ zg}{At!iWCzOH37!ytcPeC`(({ovP7M5Y@bYYMZ}P2Z3=Y_hT)4DRk}wfeIo%q*M9UvXYJq!-@Ly79m5aLD{hf@BzQB>FdQ4mw z6$@vzSKF^Gnzc9vbccii)==~9H#KW<6)Uy1wb~auBn6s`ct!ZEos`WK8e2%<00b%# zY9Nvnmj@V^K(a_38dw-S*;G-(i(ETuIwyirs?$FFW@|66a38k+a%GLmucL%Wc8qk3 z?h_4!?4Y-xt)ry)>J`SuY**fuq2>u+)VZ+_1Egzctb*xJ6+7q`K$^f~r|!i?(07CD zH!)C_uerf-AHNa?6Y61D_MjGu*|wcO+ZMOo4q2bWpvjEWK9yASk%)QhwZS%N2_F4& z16D18>e%Q1mZb`R;vW{+IUoKE`y3(7p zplg5cBB)dtf^SdLd4n60oWie|(ZjgZa6L*VKq02Aij+?Qfr#1z#fwh92aV-HGd^_w zsucG24j8b|pk>BO7k8dS86>f-jBP^Sa}SF{YNn=^NU9mLOdKcAstv&GV>r zLxKHPkFxpvE8^r@MSF6UA}cG`#yFL8;kA7ccH9D=BGBtW2;H>C`FjnF^P}(G{wU;G z!LXLCbPfsGeLCQ{Ep$^~)@?v`q(uI`CxBY44osPcq@(rR-633!qa zsyb>?v%@X+e|Mg`+kRL*(;X>^BNZz{_kw5+K;w?#pReiw7eU8_Z^hhJ&fj80XQkuU z39?-z)6Fy$I`bEiMheS(iB6uLmiMd1i)cbK*9iPpl+h4x9ch7x- z1h4H;W_G?|)i`z??KNJVwgfuAM=7&Apd3vm#AT8uzQZ!NII}}@!j)eIfn53h{NmN7 zAKG6SnKP%^k&R~m5#@_4B@V?hYyHkm>0SQ@PPiw*@Tp@UhP-?w@jW?nxXuCipMW=L zH*5l*d@+jXm0tIMP_ec6Jcy6$w(gKK@xBX8@%oPaSyG;13qkFb*LuVx3{AgIyy&n3 z@R2_DcEn|75_?-v5_o~%xEt~ONB>M~tpL!nOVBLPN&e5bn5>+7o0?Nm|EGJ5 zmUbF{u|Qn?cu5}n4@9}g(G1JxtzkKv(tqwm_?1`?YSVA2IS4WI+*(2D*wh&6MIEhw z+B+2U<&E&|YA=3>?^i6)@n1&&;WGHF-pqi_sN&^C9xoxME5UgorQ_hh1__zzR#zVC zOQt4q6>ME^iPJ37*(kg4^=EFqyKH@6HEHXy79oLj{vFqZGY?sVjk!BX^h$SFJlJnv z5uw~2jLpA)|0=tp>qG*tuLru?-u`khGG2)o{+iDx&nC}eWj3^zx|T`xn5SuR;Aw8U z`p&>dJw`F17@J8YAuW4=;leBE%qagVTG5SZdh&d)(#ZhowZ|cvWvGMMrfVsbg>_~! z19fRz8CSJdrD|Rl)w!uznBF&2-dg{>y4l+6(L(vzbLA0Bk&`=;oQQ>(M8G=3kto_) zP8HD*n4?MySO2YrG6fwSrVmnesW+D&fxjfEmp=tPd?RKLZJcH&K(-S+x)2~QZ$c(> zru?MND7_HPZJVF%wX(49H)+~!7*!I8w72v&{b={#l9yz+S_aVPc_So%iF8>$XD1q1 zFtucO=rBj0Ctmi0{njN8l@}!LX}@dwl>3yMxZ;7 z0Ff2oh8L)YuaAGOuZ5`-p%Z4H@H$;_XRJQ|&(MhO78E|nyFa158gAxG^SP(vGi^+< zChY}o(_=ci3Wta#|K6MVljNe0T$%Q5ylx-v`R)r8;3+VUpp-)7T`-Y&{Zk z*)1*2MW+_eOJtF5tCMDV`}jg-R(_IzeE9|MBKl;a7&(pCLz}5<Zf+)T7bgNUQ_!gZtMlw=8doE}#W+`Xp~1DlE=d5SPT?ymu!r4z%&#A-@x^=QfvDkfx5-jz+h zoZ1OK)2|}_+UI)i9%8sJ9X<7AA?g&_Wd7g#rttHZE;J*7!e5B^zdb%jBj&dUDg4&B zMMYrJ$Z%t!5z6=pMGuO-VF~2dwjoXY+kvR>`N7UYfIBMZGP|C7*O=tU z2Tg_xi#Q3S=1|=WRfZD;HT<1D?GMR%5kI^KWwGrC@P2@R>mDT^3qsmbBiJc21kip~ zZp<7;^w{R;JqZ)C4z-^wL=&dBYj9WJBh&rd^A^n@07qM$c+kGv^f+~mU5_*|eePF| z3wDo-qaoRjmIw<2DjMTG4$HP{z54_te_{W^gu8$r=q0JgowzgQPct2JNtWPUsjF8R zvit&V8$(;7a_m%%9TqPkCXYUp&k*MRcwr*24>hR! z$4c#E=PVE=P4MLTUBM z7#*RDe0}=B)(3cvNpOmWa*eH#2HR?NVqXdJ=hq);MGD07JIQQ7Y0#iD!$C+mk7x&B zMwkS@H%>|fmSu#+ zI!}Sb(%o29Vkp_Th>&&!k7O>Ba#Om~B_J{pT7BHHd8(Ede(l`7O#`_}19hr_?~JP9 z`q(`<)y>%)x;O7)#-wfCP{?llFMoH!)ZomgsOYFvZ1DxrlYhkWRw#E-#Qf*z@Y-EQ z1~?_=c@M4DO@8AzZ2hKvw8CgitzI9yFd&N1-{|vP#4IqYb*#S0e3hrjsEGlnc4xwk z4o!0rxpUt8j&`mJ8?+P8G{m^jbk)bo_UPM+ifW*y-A*et`#_Ja_3nYyRa9fAG1Xr5 z>#AM_@PY|*u)DGRWJihZvgEh#{*joJN28uN7;i5{kJ*Gb-TERfN{ERe_~$Es~NJCpdKLRvdj4658uYYx{ng7I<6j~w@p%F<7a(Ssib|j z51;=Py(Nu*#hnLx@w&8X%=jrADn3TW>kplnb zYbFIWWVQXN7%Cwn6KnR)kYePEBmvM45I)UJb$)ninpdYg3a5N6pm_7Q+9>!_^xy?k za8@tJ@OOs-pRAAfT>Nc2x=>sZUs2!9Dwa%TTmDggH4fq(x^MW>mcRyJINlAqK$YQCMgR8`>6=Sg$ zFnJZsA8xUBXIN3i70Q%8px@yQPMgVP=>xcPI38jNJK<=6hC={a07+n@R|$bnhB)X$ z(Zc%tadp70vBTnW{OUIjTMe38F}JIH$#A}PB&RosPyFZMD}q}5W%$rh>5#U;m`z2K zc(&WRxx7DQLM-+--^w*EWAIS%bi>h587qkwu|H=hma3T^bGD&Z!`u(RKLeNZ&pI=q$|HOcji(0P1QC!YkAp*u z3%S$kumxR}jU<@6`;*-9=5-&LYRA<~uFrwO3U0k*4|xUTp4ZY7;Zbjx|uw&BWU$zK(w55pWa~#=f$c zNDW0O68N!xCy>G}(CX=;8hJLxAKn@Aj(dbZxO8a$+L$jK8$N-h@4$i8)WqD_%Snh4 zR?{O%k}>lr>w$b$g=VP8mckcCrjnp>uQl5F_6dPM8FWRqs}h`DpfCv20uZhyY~tr8 zkAYW4#yM;*je)n=EAb(q@5BWD8b1_--m$Q-3wbh1hM{8ihq7UUQfg@)l06}y+#=$( z$x>oVYJ47zAC^>HLRE-!HitjUixP6!R98WU+h>zct7g4eD;Mj#FL*a!VW!v-@b(Jv zj@@xM5noCp5%Vk3vY{tyI#oyDV7<$`KG`tktVyC&0DqxA#>V;-3oH%NW|Q&=UQ&zU zXNIT67J4D%5R1k#bW0F}TD`hlW7b)-=-%X4;UxQ*u4bK$mTAp%y&-(?{sXF%e_VH6 zTkt(X)SSN|;8q@8XX6qfR;*$r#HbIrvOj*-5ND8RCrcw4u8D$LXm5zlj@E5<3S0R# z??=E$p{tOk96$SloZ~ARe5`J=dB|Nj?u|zy2r(-*(q^@YwZiTF@QzQyPx_l=IDKa) zqD@0?IHJqSqZ_5`)81?4^~`yiGh6>7?|dKa8!e|}5@&qV!Iu9<@G?E}Vx9EzomB3t zEbMEm$TKGwkHDpirp;FZD#6P5qIlQJ8}rf;lHoz#h4TFFPYmS3+8(13_Mx2`?^=8S z|0)0&dQLJTU6{b%*yrpQe#OKKCrL8}YKw+<#|m`SkgeoN69TzIBQOl_Yg)W*w?NW) z*WxhEp$zQBBazJSE6ygu@O^!@Fr46j=|K`Mmb~xbggw7<)BuC@cT@Bwb^k?o-A zKX^9AyqR?zBtW5UA#siILztgOp?r4qgC`9jYJG_fxlsVSugGprremg-W(K0{O!Nw-DN%=FYCyfYA3&p*K>+|Q}s4rx#CQK zNj^U;sLM#q8}#|PeC$p&jAjqMu(lkp-_50Y&n=qF9`a3`Pr9f;b`-~YZ+Bb0r~c+V z*JJ&|^T{}IHkwjNAaM^V*IQ;rk^hnnA@~?YL}7~^St}XfHf6OMMCd9!vhk#gRA*{L zp?&63axj|Si%^NW05#87zpU_>QpFNb+I00v@cHwvdBn+Un)n2Egdt~LcWOeBW4Okm zD$-e~RD+W|UB;KQ;a7GOU&%p*efGu2$@wR74+&iP8|6#_fmnh^WcJLs)rtz{46);F z4v0OL{ZP9550>2%FE(;SbM*#sqMl*UXOb>ch`fJ|(*bOZ9=EB1+V4fkQ)hjsm3-u^Pk-4ji_uDDHdD>84tER!MvbH`*tG zzvbhBR@}Yd`azQGavooV=<WbvWLlO#x`hyO34mKcxrGv=`{ssnP=0Be5#1B;Co9 zh{TR>tjW2Ny$ZxJpYeg57#0`GP#jxDCU0!H15nL@@G*HLQcRdcsUO3sO9xvtmUcc{F*>FQZcZ5bgwaS^k-j5mmt zI7Z{Xnoml|A(&_{imAjK!kf5>g(oDqDI4C{;Bv162k8sFNr;!qPa2LPh>=1n z=^_9)TsLDvTqK7&*Vfm5k;VXjBW^qN3Tl&}K=X5)oXJs$z3gk0_+7`mJvz{pK|FVs zHw!k&7xVjvY;|(Py<;J{)b#Yjj*LZO7x|~pO4^MJ2LqK3X;Irb%nf}L|gck zE#55_BNsy6m+W{e zo!P59DDo*s@VIi+S|v93PwY6d?CE=S&!JLXwE9{i)DMO*_X90;n2*mPDrL%{iqN!?%-_95J^L z=l<*{em(6|h7DR4+4G3Wr;4*}yrBkbe3}=p7sOW1xj!EZVKSMSd;QPw>uhKK z#>MlS@RB@-`ULv|#zI5GytO{=zp*R__uK~R6&p$q{Y{iNkg61yAgB8C^oy&``{~FK z8hE}H&nIihSozKrOONe5Hu?0Zy04U#0$fB7C6y~?8{or}KNvP)an=QP&W80mj&8WL zEZQF&*FhoMMG6tOjeiCIV;T{I>jhi9hiUwz?bkX3NS-k5eWKy)Mo_orMEg4sV6R6X&i-Q%JG;Esl+kLpn@Bsls9O|i9z`tKB^~1D5)RIBB&J<6T@a4$pUvh$IR$%ubH)joi z!7>ON0DPwx=>0DA>Bb^c?L8N0BBrMl#oDB+GOXJh;Y&6I)#GRy$W5xK%a;KS8BrER zX)M>Rdoc*bqP*L9DDA3lF%U8Yzb6RyIsW@}IKq^i7v&{LeIc=*ZHIbO68x=d=+0T( zev=DT9f|x!IWZNTB#N7}V4;9#V$%Wo0%g>*!MdLOEU>My0^gni9ocID{$g9ytD!gy zKRWT`DVN(lcYjR|(}f0?zgBa3SwunLfAhx><%u0uFkrdyqlh8_g zDKt#R6rA2(Vm2LW_>3lBNYKG_F{TEnnKWGGC15y&OebIRhFL4TeMR*v9i0wPoK#H< zu4){s4K&K)K(9~jgGm;H7lS7y_RYfS;&!Oj5*eqbvEcW^a*i67nevzOZxN6F+K~A%TYEtsAVsR z@J=1hc#Dgs7J2^FL|qV&#WBFQyDtEQ2kPO7m2`)WFhqAob)Y>@{crkil6w9VoA?M6 zADGq*#-hyEVhDG5MQj677XmcWY1_-UO40QEP&+D)rZoYv^1B_^w7zAvWGw&pQyCyx zD|ga$w!ODOxxGf_Qq%V9Z7Q2pFiUOIK818AGeZ-~*R zI1O|SSc=3Z?#61Rd|AXx2)K|F@Z1@x!hBBMhAqiU)J=U|Y)T$h3D?ZPPQgkSosnN! zIqw-t$0fqsOlgw3TlHJF*t$Q@bg$9}A3X=cS@-yU3_vNG_!#9}7=q7!LZ?-%U26W4 z$d>_}*s1>Ac%3uFR;tnl*fNlylJ)}r2^Q3&@+is3BIv<}x>-^_ng;jhdaM}6Sg3?p z0jS|b%QyScy3OQ(V*~l~bK>VC{9@FMuW_JUZO?y(V?LKWD6(MXzh}M3r3{7b4eB(#`(q1m{>Be%_<9jw8HO!x#yF6vez$c#kR+}s zZO-_;25Sxngd(}){zv?ccbLqRAlo;yog>4LH&uZUK1n>x?u49C)Y&2evH5Zgt~666 z_2_z|H5AO5Iqxv_Bn~*y1qzRPcob<+Otod5Xd2&z=C;u+F}zBB@b^UdGdUz|s!H}M zXG%KiLzn3G?FZgdY&3pV$nSeY?ZbU^jhLz9!t0K?ep}EFNqR1@E!f*n>x*!uO*~JF zW9UXWrVgbX1n#76_;&0S7z}(5n-bqnII}_iDsNqfmye@)kRk`w~1 z6j4h4BxcPe6}v)xGm%=z2#tB#^KwbgMTl2I*$9eY|EWAHFc3tO48Xo5rW z5oHD!G4kb?MdrOHV=A+8ThlIqL8Uu+7{G@ zb)cGBm|S^Eh5= z^E^SZ=yeC;6nNCdztw&TdnIz}^Of@Ke*@vjt)0g>Y!4AJvWiL~e7+9#Ibhe)> ziNwh>gWZL@FlWc)wzihocz+%+@*euwXhW%Hb>l7tf8aJe5_ZSH1w-uG|B;9qpcBP0 zM`r1Hu#htOl)4Cl1c7oY^t0e4Jh$-I(}M5kzWqh{F=g&IM#JiC`NDSd@BCKX#y<P@Gwl$3a3w z6<(b|K(X5FIR22M)sy$4jY*F4tT{?wZRI+KkZFb<@j@_C316lu1hq2hA|1wCmR+S@ zRN)YNNE{}i_H`_h&VUT5=Y(lN%m?%QX;6$*1P}K-PcPx>*S55v)qZ@r&Vcic-sjkm z! z=nfW&X`}iAqa_H$H%z3Tyz5&P3%+;93_0b;zxLs)t#B|up}JyV$W4~`8E@+BHQ+!y zuIo-jW!~)MN$2eHwyx-{fyGjAWJ(l8TZtUp?wZWBZ%}krT{f*^fqUh+ywHifw)_F> zp76_kj_B&zFmv$FsPm|L7%x-j!WP>_P6dHnUTv!9ZWrrmAUteBa`rT7$2ixO;ga8U z3!91micm}{!Btk+I%pMgcKs?H4`i+=w0@Ws-CS&n^=2hFTQ#QeOmSz6ttIkzmh^`A zYPq)G1l3h(E$mkyr{mvz*MP`x+PULBn%CDhltKkNo6Uqg!vJ#DA@BIYr9TQ`18Un2 zv$}BYzOQuay9}w(?JV63F$H6WmlYPPpH=R|CPb%C@BCv|&Q|&IcW7*LX?Q%epS z`=CPx{1HnJ9_46^=0VmNb>8JvMw-@&+V8SDLRYsa>hZXEeRbtf5eJ>0@Ds47zIY{N z42EOP9J8G@MXXdeiPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$?lu1NER9Fe^SItioK@|V(ZWmgL zZT;XwPgVuWM>O%^|Dc$VK;n&?9!&g5)aVsG8cjs5UbtxVVnQNOV~7Mrg3+jnU;rhE z6fhW6P)R>_eXrXo-RW*y6RQ_qcb^s1wTu$TwriZ`=JUws>vRi}5x}MW1MR#7p|gIWJlaLK;~xaN}b< z<-@=RX-%1mt`^O0o^~2=CD7pJ<<$Rp-oUL-7PuG>do^5W_Mk#unlP}6I@6NPxY`Q} zuXJF}!0l)vwPNAW;@5DjPRj?*rZxl zwn;A(cFV!xe^CUu+6SrN?xe#mz?&%N9QHf~=KyK%DoB8HKC)=w=3E?1Bqj9RMJs3U z5am3Uv`@+{jgqO^f}Lx_Jp~CoP3N4AMZr~4&d)T`R?`(M{W5WWJV^z~2B|-oih@h^ zD#DuzGbl(P5>()u*YGo*Och=oRr~3P1wOlKqI)udc$|)(bacG5>~p(y>?{JD7nQf_ z*`T^YL06-O>T(s$bi5v~_fWMfnE7Vn%2*tqV|?~m;wSJEVGkNMD>+xCu#um(7}0so zSEu7?_=Q64Q5D+fz~T=Rr=G_!L*P|(-iOK*@X8r{-?oBlnxMNNgCVCN9Y~ocu+?XA zjjovJ9F1W$Nf!{AEv%W~8oahwM}4Ruc+SLs>_I_*uBxdcn1gQ^2F8a*vGjgAXYyh? zWCE@c5R=tbD(F4nL9NS?$PN1V_2*WR?gjv3)4MQeizuH`;sqrhgykEzj z593&TGlm3h`sIXy_U<7(dpRXGgp0TB{>s?}D{fwLe>IV~exweOfH!qM@CV5kib!YA z6O0gvJi_0J8IdEvyP#;PtqP*=;$iI2t(xG2YI-e!)~kaUn~b{6(&n zp)?iJ`z2)Xh%sCV@BkU`XL%_|FnCA?cVv@h*-FOZhY5erbGh)%Q!Av#fJM3Csc_g zC2I6x%$)80`Tkz#KRA!h1FzY`?0es3t!rKDT5EjPe6B=BLPr7s0GW!if;Ip^!AmGW zL;$`Vdre+|FA!I4r6)keFvAx3M#1`}ijBHDzy)3t0gwjl|qC2YB`SSxFKHr(oY#H$)x{L$LL zBdLKTlsOrmb>T0wd=&6l3+_Te>1!j0OU8%b%N342^opKmT)gni(wV($s(>V-fUv@0p8!f`=>PxC|9=nu ze{ToBBj8b<{PLfXV$h8YPgA~E!_sF9bl;QOF{o6t&JdsX?}rW!_&d`#wlB6T_h;Xf zl{4Tz5>qjF4kZgjO7ZiLPRz_~U@k5%?=30+nxEh9?s78gZ07YHB`FV`4%hlQlMJe@J`+e(qzy+h(9yY^ckv_* zb_E6o4p)ZaWfraIoB2)U7_@l(J0O%jm+Or>8}zSSTkM$ASG^w3F|I? z$+eHt7T~04(_WfKh27zqS$6* zzyy-ZyqvSIZ0!kkSvHknm_P*{5TKLQs8S6M=ONuKAUJWtpxbL#2(_huvY(v~Y%%#~ zYgsq$JbLLprKkV)32`liIT$KKEqs$iYxjFlHiRNvBhxbDg*3@Qefw4UM$>i${R5uB zhvTgmqQsKA{vrKN;TSJU2$f9q=y{$oH{<)woSeV>fkIz6D8@KB zf4M%v%f5U2?<8B(xn}xV+gWP?t&oiapJhJbfa;agtz-YM7=hrSuxl8lAc3GgFna#7 zNjX7;`d?oD`#AK+fQ=ZXqfIZFEk{ApzjJF0=yO~Yj{7oQfXl+6v!wNnoqwEvrs81a zGC?yXeSD2NV!ejp{LdZGEtd1TJ)3g{P6j#2jLR`cpo;YX}~_gU&Gd<+~SUJVh+$7S%`zLy^QqndN<_9 zrLwnXrLvW+ew9zX2)5qw7)zIYawgMrh`{_|(nx%u-ur1B7YcLp&WFa24gAuw~& zKJD3~^`Vp_SR$WGGBaMnttT)#fCc^+P$@UHIyBu+TRJWbcw4`CYL@SVGh!X&y%!x~ zaO*m-bTadEcEL6V6*{>irB8qT5Tqd54TC4`h`PVcd^AM6^Qf=GS->x%N70SY-u?qr>o2*OV7LQ=j)pQGv%4~z zz?X;qv*l$QSNjOuQZ>&WZs2^@G^Qas`T8iM{b19dS>DaXX~=jd4B2u`P;B}JjRBi# z_a@&Z5ev1-VphmKlZEZZd2-Lsw!+1S60YwW6@>+NQ=E5PZ+OUEXjgUaXL-E0fo(E* zsjQ{s>n33o#VZm0e%H{`KJi@2ghl8g>a~`?mFjw+$zlt|VJhSU@Y%0TWs>cnD&61fW4e0vFSaXZa4-c}U{4QR8U z;GV3^@(?Dk5uc@RT|+5C8-24->1snH6-?(nwXSnPcLn#X_}y3XS)MI_?zQ$ZAuyg+ z-pjqsw}|hg{$~f0FzmmbZzFC0He_*Vx|_uLc!Ffeb8#+@m#Z^AYcWcZF(^Os8&Z4g zG)y{$_pgrv#=_rV^D|Y<_b@ICleUv>c<0HzJDOsgJb#Rd-Vt@+EBDPyq7dUM9O{Yp zuGUrO?ma2wpuJuwl1M=*+tb|qx7Doj?!F-3Z>Dq_ihFP=d@_JO;vF{iu-6MWYn#=2 zRX6W=`Q`q-+q@Db|6_a1#8B|#%hskH82lS|9`im0UOJn?N#S;Y0$%xZw3*jR(1h5s z?-7D1tnIafviko>q6$UyqVDq1o@cwyCb*})l~x<@s$5D6N=-Uo1yc49p)xMzxwnuZ zHt!(hu-Ek;Fv4MyNTgbW%rPF*dB=;@r3YnrlFV{#-*gKS_qA(G-~TAlZ@Ti~Yxw;k za1EYyX_Up|`rpbZ0&Iv#$;eC|c0r4XGaQ-1mw@M_4p3vKIIpKs49a8Ns#ni)G314Z z8$Ei?AhiT5dQGWUYdCS|IC7r z=-8ol>V?u!n%F*J^^PZ(ONT&$Ph;r6X;pj|03HlDY6r~0g~X#zuzVU%a&!fs_f|m?qYvg^Z{y?9Qh7Rn?T*F%7lUtA6U&={HzhYEzA`knx1VH> z{tqv?p@I(&ObD5L4|YJV$QM>Nh-X3cx{I&!$FoPC_2iIEJfPk-$;4wz>adRu@n`_y z_R6aN|MDHdK;+IJmyw(hMoDCFCQ(6?hCAG5&7p{y->0Uckv# zvooVuu04$+pqof777ftk<#42@KQ((5DPcSMQyzGOJ{e9H$a9<2Qi_oHjl{#=FUL9d z+~0^2`tcvmp0hENwfHR`Ce|<1S@p;MNGInXCtHnrDPXCKmMTZQ{HVm_cZ>@?Wa6}O zHsJc7wE)mc@1OR2DWY%ZIPK1J2p6XDO$ar`$RXkbW}=@rFZ(t85AS>>U0!yt9f49^ zA9@pc0P#k;>+o5bJfx0t)Lq#v4`OcQn~av__dZ-RYOYu}F#pdsl31C^+Qgro}$q~5A<*c|kypzd} ziYGZ~?}5o`S5lw^B{O@laad9M_DuJle- z*9C7o=CJh#QL=V^sFlJ0c?BaB#4bV^T(DS6&Ne&DBM_3E$S^S13qC$7_Z?GYXTpR@wqr70wu$7+qvf-SEUa5mdHvFbu^7ew!Z1a^ zo}xKOuT*gtGws-a{Tx}{#(>G~Y_h&5P@Q8&p!{*s37^QX_Ibx<6XU*AtDOIvk|^{~ zPlS}&DM5$Ffyu-T&0|KS;Wnaqw{9DB&B3}vcO14wn;)O_e@2*9B&0I_ zZz{}CMxx`hv-XouY>^$Y@J(_INeM>lIQI@I>dBAqq1)}?Xmx(qRuX^i4IV%=MF306 z9g)i*79pP%_7Ex?m6ag-4Tlm=Z;?DQDyC-NpUIb#_^~V_tsL<~5<&;Gf2N+p?(msn zzUD~g>OoW@O}y0@Z;RN)wjam`CipmT&O7a|YljZqU=U86 zedayEdY)2F#BJ6xvmW8K&ffdS*0!%N<%RB!2~PAT4AD*$W7yzHbX#Eja9%3aD+Ah2 zf#T;XJW-GMxpE=d4Y>}jE=#U`IqgSoWcuvgaWQ9j1CKzG zDkoMDDT)B;Byl3R2PtC`ip=yGybfzmVNEx{xi_1|Cbqj>=FxQc{g`xj6fIfy`D8fA z##!-H_e6o0>6Su&$H2kQTujtbtyNFeKc}2=|4IfLTnye#@$Au7Kv4)dnA;-fz@D_8 z)>irG$)dkBY~zX zC!ZXLy*L3xr6cb70QqfN#Q>lFIc<>}>la4@3%7#>a1$PU&O^&VszpxLC%*!m-cO{B z-Y}rQr4$84(hvy#R69H{H zJ*O#uJh)TF6fbXy;fZkk%X=CjsTK}o5N1a`d7kgYYZLPxsHx%9*_XN8VWXEkVJZ%A z1A+5(B;0^{T4aPYr8%i@i32h)_)|q?9vws)r+=5u)1YNftF5mknwfd*%jXA2TeP}Z zQ!m?xJ3?9LpPM?_A3$hQ1QxNbR&}^m z!F999s?p^ak#C4NM_x2p9FoXWJ$>r?lJ)2bG)sX{gExgLA2s5RwHV!h6!C~d_H||J z>9{E{mEv{Z1z~65Vix@dqM4ZqiU|!)eWX$mwS5mLSufxbpBqqS!jShq1bmwCR6 z4uBri7ezMeS6ycaXPVu(i2up$L; zjpMtB`k~WaNrdgM_R=e#SN?Oa*u%nQy01?()h4A(jyfeNfx;5o+kX?maO4#1A^L}0 zYNyIh@QVXIFiS0*tE}2SWTrWNP3pH}1Vz1;E{@JbbgDFM-_Mky^7gH}LEhl~Ve5PexgbIyZ(IN%PqcaV@*_`ZFb=`EjspSz%5m2E34BVT)d=LGyHVz@-e%9Ova*{5@RD;7=Ebkc2GP%pIP^P7KzKapnh`UpH?@h z$RBpD*{b?vhohOKf-JG3?A|AX|2pQ?(>dwIbWhZ38GbTm4AImRNdv_&<99ySX;kJ| zo|5YgbHZC#HYgjBZrvGAT4NZYbp}qkVSa;C-LGsR26Co+i_HM&{awuO9l)Ml{G8zD zs$M8R`r+>PT#Rg!J(K6T4xHq7+tscU(}N$HY;Yz*cUObX7J7h0#u)S7b~t^Oj}TBF zuzsugnst;F#^1jm>22*AC$heublWtaQyM6RuaquFd8V#hJ60Z3j7@bAs&?dD#*>H0SJaDwp%U~27>zdtn+ z|8sZzklZy$%S|+^ie&P6++>zbrq&?+{Yy11Y>@_ce@vU4ZulS@6yziG6;iu3Iu`M= zf3rcWG<+3F`K|*(`0mE<$89F@jSq;j=W#E>(R}2drCB7D*0-|D;S;(;TwzIJkGs|q z2qH{m_zZ+el`b;Bv-#bQ>}*VPYC|7`rgBFf2oivXS^>v<&HHTypvd4|-zn|=h=TG{ z05TH2+{T%EnADO>3i|CB zCu60#qk`}GW{n4l-E$VrqgZGbI zbQW690KgZt4U3F^5@bdO1!xu~p@7Y~*_FfWg2CdvED5P5#w#V46LH`<&V0{t&Ml~4 zHNi7lIa+#i+^Z6EnxO7KJQw)wD)4~&S-Ki8)3=jpqxmx6c&zU&<&h%*c$I(5{1HZT zc9WE}ijcWJiVa^Q^xC|WX0habl89qycOyeViIbi(LFsEY_8a|+X^+%Qv+W4vzj>`y zpuRnjc-eHNkvXvI_f{=*FX=OKQzT?bck#2*qoKTHmDe>CDb&3AngA1O)1b}QJ1Tun z_<@yVEM>qG7664Pa@dzL@;DEh`#?yM+M|_fQS<7yv|i*pw)|Z8)9IR+QB7N3v3K(wv4OY*TXnH&X0nQB}?|h2XQeGL^q~N7N zDFa@x0E(UyN7k9g%IFq7Sf+EAfE#K%%#`)!90_)Dmy3Bll&e1vHQyPA87TaF(xbqMpDntVp?;8*$87STop$!EAnGhZ?>mqPJ(X zFsr336p3P{PpZCGn&^LP(JjnBbl_3P3Kcq+m}xVFMVr1zdCPJMDIV_ki#c=vvTwbU z*gKtfic&{<5ozL6Vfpx>o2Tts?3fkhWnJD&^$&+Mh5WGGyO7fG@6WDE`tEe(8<;+q z@Ld~g08XDzF8xtmpIj`#q^(Ty{Hq>t*v`pedHnuj(0%L(%sjkwp%s}wMd!a<*L~9T z9MM@s)Km~ogxlqEhIw5(lc46gCPsSosUFsgGDr8H{mj%OzJz{N#;bQ;KkV+ZWA1(9 zu0PXzyh+C<4OBYQ0v3z~Lr;=C@qmt8===Ov2lJ1=DeLfq*#jgT{YQCuwz?j{&3o_6 zsqp2Z_q-YWJg?C6=!Or|b@(zxTlg$ng2eUQzuC<+o)k<6^9ju_Z*#x+oioZ5T8Z_L zz9^A1h2eFS0O5muq8;LuDKwOv4A9pxmOjgb6L*i!-(0`Ie^d5Fsgspon%X|7 zC{RRXEmYn!5zP9XjG*{pLa)!2;PJB2<-tH@R7+E1cRo=Wz_5Ko8h8bB$QU%t9#vol zAoq?C$~~AsYC|AQQ)>>7BJ@{Cal)ZpqE=gjT+Juf!RD-;U0mbV1ED5PbvFD6M=qj1 zZ{QERT5@(&LQ~1X9xSf&@%r|3`S#ZCE=sWD`D4YQZ`MR`G&s>lN{y2+HqCfvgcw3E z-}Kp(dfGG?V|97kAHQX+OcKCZS`Q%}HD6u*e$~Ki&Vx53&FC!x94xJd4F2l^qQeFO z?&JdmgrdVjroKNJx64C!H&Vncr^w zzR#XI}Dn&o8jB~_YlVM^+#0W(G1LZH5K^|uYT@KSR z^Y5>^*Bc45E1({~EJB(t@4n9gb-eT#s@@7)J^^<_VV`Pm!h7av8XH6^5zO zOcQBhTGr;|MbRsgxCW69w{bl4EW#A~);L?d4*y#j8Ne=Z@fmJP0k4{_cQ~KA|Y#_#BuUiYx8y*za3_6Y}c=GSe7(2|KAfhdzud!Zq&}j)=o4 z7R|&&oX7~e@~HmyOOsCCwy`AR+deNjZ3bf6ijI_*tKP*_5JP3;0d;L_p(c>W1b%sG zJ*$wcO$ng^aW0E(5ldckV9unU7}OB7s?Wx(761?1^&8tA5y0_(ieV>(x-e@}1`lWC z-YH~G$D>#ud!SxK2_Iw{K%92=+{4yb-_XC>ji&j7)1ofp(OGa4jjF;Hd*`6YQL+Jf zffg+6CPc8F@EDPN{Kn96yip;?g@)qgkPo^nVKFqY?8!=h$G$V=<>%5J&iVjwR!7H0 z$@QL|_Q81I;Bnq8-5JyNRv$Y>`sWl{qhq>u+X|)@cMlsG!{*lu?*H`Tp|!uv z9oEPU1jUEj@ueBr}%Y)7Luyi)REaJV>eQ{+uy4uh0ep0){t;OU8D*RZ& zE-Z-&=BrWQLAD^A&qut&4{ZfhqK1ZQB0fACP)=zgx(0(o-`U62EzTkBkG@mXqbjXm z>w`HNeQM?Is&4xq@BB(K;wv5nI6EXas)XXAkUuf}5uSrZLYxRCQPefn-1^#OCd4aO zzF=dQ*CREEyWf@n6h7(uXLNgJIwGp#Xrsj6S<^bzQ7N0B0N{XlT;`=m9Olg<>KL}9 zlp>EKTx-h|%d1Ncqa=wnQEuE;sIO-f#%Bs?g4}&xS?$9MG?n$isHky0caj za8W+B^ERK#&h?(x)7LLpOqApV5F>sqB`sntV%SV>Q1;ax67qs+WcssfFeF3Xk=e4^ zjR2^(%K1oBq%0%Rf!y&WT;lu2Co(rHi|r1_uW)n{<7fGc-c=ft7Z0Q}r4W$o$@tQF#i?jDBwZ8h+=SC}3?anUp3mtRVv9l#H?-UD;HjTF zQ*>|}e=6gDrgI9p%c&4iMUkQa4zziS$bO&i#DI$Wu$7dz7-}XLk%!US^XUIFf2obO zFCTjVEtkvYSKWB;<0C;_B{HHs~ax_48^Cml*mjfBC5*7^HJZiLDir(3k&BerVIZF8zF;0q80eX8c zPN4tc+Dc5DqEAq$Y3B3R&XPZ=AQfFMXv#!RQnGecJONe0H;+!f^h5x0wS<+%;D}MpUbTNUBA}S2n&U59-_5HKr{L^jPsV8B^%NaH|tUr)mq=qCBv_- ziZ1xUp(ZzxUYTCF@C}To;u60?RIfTGS?#JnB8S8@j`TKPkAa)$My+6ziGaBcA@){d z91)%+v2_ba7gNecdj^8*I4#<11l!{XKl6s0zkXfJPxhP+@b+5ev{a>p*W-3*25c&} zmCf{g9mPWVQ$?Sp*4V|lT@~>RR)9iNdN^7KT@>*MU3&v^3e?=NTbG9!h6C|9zO097 zN{Qs6YwR-5$)~ z`b~qs`a1Dbx8P>%V=1XGjBptMf%P~sl1qbHVm1HYpY|-Z^Dar8^HqjIw}xaeRlsYa zJ_@Apy-??`gxPmb`m`0`z`#G7*_C}qiSZe~l2z65tE~IwMw$1|-u&t|z-8SxliH00 zlh1#kuqB56s+E&PWQ7Nz17?c}pN+A@-c^xLqh(j;mS|?>(Pf7(?qd z5q@jkc^nA&!K-}-1P=Ry0yyze0W!+h^iW}7jzC1{?|rEFFWbE^Yu7Y}t?jmP-D$f+ zmqFT7nTl0HL|4jwGm7w@a>9 zKD)V~+g~ysmei$OT5}%$&LK8?ib|8aY|>W3;P+0B;=oD=?1rg+PxKcP(d;OEzq1CKA&y#boc51P^ZJPPS)z5 zAZ)dd2$glGQXFj$`XBBJyl2y-aoBA8121JC9&~|_nY>nkmW>TLi%mWdn-^Jks-Jv| zSR*wij;A3Fcy8KsDjQ15?Z9oOj|Qw2;jgJiq>dxG(2I2RE- z$As!#zSFIskebqU2bnoM^N<4VWD2#>!;saPSsY8OaCCQqkCMdje$C?Sp%V}f2~tG5 z0whMYk6tcaABwu*x)ak@n4sMElGPX1_lmv@bgdI2jPdD|2-<~Jf`L`@>Lj7{<-uLQ zE3S_#3e10q-ra=vaDQ42QUY^@edh>tnTtpBiiDVUk5+Po@%RmuTntOlE29I4MeJI?;`7;{3e4Qst#i-RH6s;>e(Sc+ubF2_gwf5Qi%P!aa89fx6^{~A*&B4Q zKTF|Kx^NkiWx=RDhe<{PWXMQ;2)=SC=yZC&mh?T&CvFVz?5cW~ritRjG2?I0Av_cI z)=s!@MXpXbarYm>Kj0wOxl=eFMgSMc?62U#2gM^li@wKPK9^;;0_h7B>F>0>I3P`{ zr^ygPYp~WVm?Qbp6O3*O2)(`y)x>%ZXtztz zMAcwKDr=TCMY!S-MJ8|2MJCVNUBI0BkJV6?(!~W!_dC{TS=eh}t#X+2D>Kp&)ZN~q zvg!ogxUXu^y(P*;Q+y_rDoGeSCYxkaGPldDDx)k;ocJvvGO#1YKoQLHUf2h_pjm&1 zqh&!_KFH03FcJvSdfgUYMp=5EpigZ*8}7N_W%Ms^WSQ4hH`9>3061OEcxmf~TcYn5_oHtscWn zo5!ayj<_fZ)vHu3!A!7M;4y1QIr8YGy$P2qDD_4+T8^=^dB6uNsz|D>p~4pF3Nrb6 zcpRK*($<~JUqOya#M1=#IhOZ zG)W+rJS-x(6EoVz)P zsSo>JtnChdj9^);su%SkFG~_7JPM zEDz3gk2T7Y%x>1tWyia|op(ilEzvAujW?Xwlw>J6d7yEi8E zv30riR|a_MM%ZZX&n!qm0{2agq(s?x9E@=*tyT$nND+{Djpm7Rsy!+c$j+wqMwTOF zZL8BQ|I`<^bGW)5apO{lh(Asqen?_U`$_n0-Ob~Yd%^89oEe%9yGumQ_8Be+l2k+n zCxT%s?bMpv|AdWP7M1LQwLm|x+igA~;+iK-*+tClF&ueX_V}>=4gvZ01xpubQWXD_ zi?Un>&3=$fu)dgk-Z;0Ll}HK5_YM->l^Czrd0^cJ))(DwL2g3aZuza7ga9^|mT_70 z))}A}r1#-(9cxtn<9jGRwOB4hb9kK@YCgjfOM-90I$8@l=H^`K$cyhe2mTM|FY9vW znH~h)I<_aa#V1xmhk?Ng@$Jw-s%a!$BI4Us+Df+?J&gKAF-M`v}j`OWKP3>6`X`tEmhe#y*(Xm$_^Ybbs=%;L7h zp7q^C*qM}Krqsinq|WolR99>_!GL#Z71Hhz|IwQQv<>Ds09B?Je(lhI1(FInO8mc} zl$RyKCUmfku+Cd^8s0|t+e}5g7M{ZPJQH=UB3(~U&(w#Bz#@DTDHy>_UaS~AtN>4O zJ-I#U@R($fgupHebcpuEBX`SZ>kN!rW$#9>s{^3`86ZRQRtYTY)hiFm_9wU3c`SC8 z-5M%g)h}3Pt|wyj#F%}pGC@VL`9&>9P+_UbudCkS%y2w&*o})hBplrB*@Z?gel5q+ z%|*59(sR9GMk3xME}wd%&k?7~J)OL`rK#4d-haC7uaU8-L@?$K6(r<0e<;y83rK&` z3Q!1rD9WkcB8WBQ|WT|$u^lkr0UL4WH4EQTJyk@5gzHb18cOte4w zS`fLv8q;PvAZyY;*Go3Qw1~5#gP0D0ERla6M6#{; zr1l?bR}Nh+OC7)4bfAs(0ZD(axaw6j9v`^jh5>*Eo&$dAnt?c|Y*ckEORIiJXfGcM zEo`bmIq6rJm`XhkXR-^3d8^RTK2;nmVetHfUNugJG(4XLOu>HJA;0EWb~?&|0abr6 zxqVp@p=b3MN^|~?djPe!=eex(u!x>RYFAj|*T$cTi*Sd3Bme7Pri1tkK9N`KtRmXf zZYNBNtik97ct1R^vamQBfo9ZUR@k*LhIg8OR9d_{iv#t)LQV91^5}K5u{eyxwOFoU zHMVq$C>tfa@uNDW^_>EmO~WYQd(@!nKmAvSSIb&hPO|}g-3985t?|R&WZXvxS}Kt2i^eRe>WHb_;-K5cM4=@AN1>E&1c$k!w4O*oscx(f=<1K6l#8Exi)U(ZiZ zdr#YTP6?m1e1dOKysUjQ^>-MR={OuD00g6+(a^cvcmn#A_%Fh3Of%(qP5nvjS1=(> z|Ld8{u%(J}%2SY~+$4pjy{()5HN2MYUjg1X9umxOMFFPdM+IwOVEs4Z(olynvT%G) zt9|#VR}%O2@f6=+6uvbZv{3U)l;C{tuc zZ{K$rut=eS%3_~fQv^@$HV6#9)K9>|0qD$EV2$G^XUNBLM|5-ZmFF!KV)$4l^KVj@ zZ4fI}Knv*K%zPqK77}B-h_V{66VrmoZP2>@^euu8Rc}#qwRwt5uEBWcJJE5*5rT2t zA4Jpx`QQ~1Sh_n_a9x%Il!t1&B~J6p54zxAJx`REov${jeuL8h8x-z=?qwMAmPK5i z_*ES)BW(NZluu#Bmn1-NUKQip_X&_WzJy~J`WYxEJQ&Gu7DD< z&F9urE;}8S{x4{yB zaq~1Zrz%8)<`prSQv$eu5@1RY2WLu=waPTrn`WK%;G5(jt^FeM;gOdvXQjYhax~_> z{bS_`;t#$RYMu-;_Dd&o+LD<5Afg6v{NK?0d8dD5ohAN?QoocETBj?y{MB)jQ%UQ}#t3j&iL!qr@#6JEajR3@^k5wgLfI9S9dT2^f`2wd z%I#Q*@Ctk@w=(u)@QC}yBvUP&fFRR-uYKJ){Wp3&$s(o~W7OzgsUIPx0|ph2L1(r*_Pa@T@mcH^JxBjh09#fgo|W#gG7}|)k&uD1iZxb0 z@|Y)W79SKj9sS&EhmTD;uI#)FE6VwQ*YAr&foK$RI5H8_ripb$^=;U%gWbrrk4!5P zXDcyscEZoSH~n6VJu8$^6LE6)>+=o#Q-~*jmob^@191+Ot1w454e3)WMliLtY6~^w zW|n#R@~{5K#P+(w+XC%(+UcOrk|yzkEes=!qW%imu6>zjdb!B#`efaliKtN}_c!Jp zfyZa`n+Nx8;*AquvMT2;c8fnYszdDA*0(R`bsof1W<#O{v%O!1IO4WZe=>XBu_D%d zOwWDaEtX%@B>4V%f1+dKqcXT>m2!|&?}(GK8e&R=&w?V`*Vj)sCetWp9lr@@{xe6a zE)JL&;p}OnOO}Nw?vFyoccXT*z*?r}E8{uPtd;4<(hmX;d$rqJhEF}I+kD+m(ke;J z7Cm$W*CSdcD=RYEBhedg>tuT{PHqwCdDP*NkHv4rvQTXkzEn*Mb0oJz&+WfWIOS4@ zzpPJ|e%a-PIwOaOC7uQcHQ-q(SE(e@fj+7oC@34wzaBNaP;cw&gm{Z8yYX?V(lIv5 zKbg*zo1m5aGA4^lwJ|bAU=j3*d8S{vp!~fLFcK8s6%Ng55_qW_d*3R%e=34aDZPfD z&Le39j|ahp6E7B0*9OVdeMNrTErFatiE+=Z!XZ^tv0y%zZKXRTBuPyP&C{5(H?t)S zKV24_-TKpOmCPzU&by8R1Q5HY^@IDoeDA9MbgizgQ*F1Er~HVmvSU>vx}pZVQ&tr| zOtZl8vfY2#L<)gZ=ba&wG~EI*Vd?}lRMCf+!b5CDz$8~be-HKMo5omk$w7p4`Mym*IR8WiTz4^kKcUo^8Hkcsu14u z`Pkg`#-Y^A%CqJ0O@UF|caAulf68@(zhqp~YjzInh7qSN7Ov%Aj(Qz%{3zW|xubJ- ztNE_u_MO7Q_585r;xD?e=Er}@U1G@BKW5v$UM((eByhH2p!^g9W}99OD8VV@7d{#H zv)Eam+^K(5>-Ot~U!R$Um3prQmM)7DyK=iM%vy>BRX4#aH7*oCMmz07YB(EL!^%F7?CA#>zXqiYDhS;e?LYPTf(bte6B ztrfvDXYG*T;ExK-w?Knt{jNv)>KMk*sM^ngZ-WiUN;=0Ev^GIDMs=AyLg2V@3R z7ugNc45;4!RPxvzoT}3NCMeK$7j#q3r_xV(@t@OPRyoKBzHJ#IepkDsm$EJRxL)A* zf{_GQYttu^OXr$jHQn}zs$Eh|s|Z!r?Yi+bS-bi+PE*lH zo|6ztu6$r_?|B~S#m>imI!kQP9`6X426uHRri!wGcK;J;`%sFM(D#*Le~W*t2uH`Q z(HEO9-c_`mhA@4QhbW+tgtt9Pzx=_*3Kh~TB$SKmU4yx-Ay&)n%PZPKg#rD4H{%Ke zdMY@rf5EAFfqtrf?Vmk&N(_d-<=bvfOdPrYwY*;5%j@O6@O#Qj7LJTk-x3LN+dEKy+X z>~U8j3Ql`exr1jR>+S4nEy+4c2f{-Q!3_9)yY758tLGg7k^=nt<6h$YE$ltA+13S<}uOg#XHe6 zZHKdNsAnMQ_RIuB;mdoZ%RWpandzLR-BnjN2j@lkBbBd+?i ze*!5mC}!Qj(Q!rTu`KrRRqp22c=hF6<^v&iCDB`n7mHl;vdclcer%;{;=kA(PwdGG zdX#BWoC!leBC4);^J^tPkPbIe<)~nYb6R3u{HvC!NOQa?DC^Q`|_@ zcz;rk`a!4rSLAS>_=b@g?Yab4%=J3Cc7pRv8?_rHMl_aK*HSPU%0pG2Fyhef_biA!aW|-(( z*RIdG&Lmk(=(nk28Q1k1Oa$8Oa-phG%Mc6dT3>JIylcMMIc{&FsBYBD^n@#~>C?HG z*1&FpYVvXOU@~r2(BUa+KZv;tZ15#RewooEM0LFb>guQN;Z0EBFMFMZ=-m$a3;gVD z)2EBD4+*=6ZF?+)P`z@DOT;azK0Q4p4>NfwDR#Pd;no|{q_qB!zk1O8QojE;>zhPu z1Q=1z^0MYHo1*``H3ex|bW-Zy==5J4fE2;g6sq6YcXMYK5i|S^9(OSw#v!3^!EB<% zZF~J~CleS`V-peStyf*I%1^R88D;+8{{qN6-t!@gTARDg^w2`uSzFZbPQ!)q^oC}m zPo8VOQxq2BaIN`pAVFGu8!{p3}(+iZ`f4ck2ygVpEZMQW38nLpj3NQx+&sAkb8`}P3- zc>N*k6AG?r}bfO6_vccTuKX+*- z7W4Q#2``P0jIHYs)F>uG#AM#I6W2)!Nu2nD5{CRV_PmkDS2ditmbd#pggqEgAo%5oC?|CP zGa0CV)wA*ko!xC7pZYkqo{10CN_e00FX5SjWkI3?@XG}}bze!(&+k2$C-C`6temSk z_YyYpB^wh3woo`B zrMSTd4T?(X-jh`FeO76C(3xsOm9s2BP_b%ospg^!#*2*o9N;tf4(X9$qc_d(()yz5 zDk@1}u_Xd+86vy5RBs?LQCuYKCGPS;E4uFOi@V%1JTK&|eRf~lp$AV#;*#O}iRI2=i3rFL8{ zA^ptDZ0l6k-mq=hUJ0x$Y@J>UNfz~I5l63H(`~*v;qX`Z{zwsQQD-!wp0D&hyB8&Z z7$R07gIKGJ^%AvQ{4KM0edM39iFRx=P^6`!<1(s0t|JbB2tXs_B_IH9#ajH0C=-n+ z`nz`fKMBKLlf?2AC+|83M+0rqR%uhNGD;uKA6jOjp7YDe^4%0fRB<^bcjlS2KF~F; zu09wh1x0&4pG&76M;x8$u`b134t=dEPBn6PV|X29<#T4F1mxGF*HOgiWU8tN@cguI z_F@o+XL7FJztR63wC|j4x_DANzcX94r7Iz-O2x$({&qd*mdLG=-Rv)uZ}UlMR+F&q zU}=lkfb0p1>1Ho){o$@}mSKIV;h*$AND7~Dl)QzpFBlSM99Kx+F7GsVK5xcR? z_4Q(Z%cgk8ST}U;;=!LwyZVu^S$>B-Waeik%wzcKTIqeX=0FP(TGQ=nxi=dsS5BYF zl@?}NT!Y!Iyos^@v7XWXA{_bV~1lxz7gC?xuXxy0_?GaN!AhRRM5>)^t%&ODd;@HN5L{MD3 zc>i2keQZVm#?NrDwbfd}_<*5^U&w0zv~n-y8=GGN-!=_`FU^cM8oVCWRFxw?BM^YD zi=Vxz4q|jwPTg+?q7_XI)-S@gQkh>w0ZUB}a{^ z_i;`Y(~fvpI!vmW*A^|P7(6+@C4UeL2WATf{P1?H5rk`5{TL zcf!CgP6Mi{MvjZS)rfo7JLDZK7M7ANd$3`{j9baD*7{#Zu-33fOYUzjvtKzR2)_T1I1s7fe&z|=)QkX;=`zX8!Byw-veM#yr;|wjO^II>!B*B z0+w%;0(=*G3V@88t!}~zx)&do(uF=073Yeh*fEhZb3Vn>t!m(9p~Y_FdV3IgR)9eT z)~e9xpI%2deTWyHlXA(7srrfc_`7ACm!R>SoIgkuF8 z!wkOhrixFy9y@)GdxAntd!!7@=L_tFD2T5OdSUO)I%yj02le`qeQ=yKq$g^h)NG;# za(0J@#VBi^5YI|QI=rq{KlxwGabZJ0dKmfWDROkcM}lUN$@DV`K7fU?8CP2H23QPi zG?YF*=Vn=kTK*#Y_{AQN&oLju|0#E=fx%YVh>S{puu&K$b;BN*jIo@VYhqPiJPzzM>#kxoy0vW9i;ne2_BIG0zyRFp<3M(iY(%*M_>q0ulV2K}Tg zkG{EWKS{i%4DUuHi%DVKy%e+Q!~Uf`>>F6NgD{{I8~nO4!VgOvtFOc7(O)X`|7n*f zxBa4CJ-v9fUUH+`7sPVvpM_C*udZ@OTGTzx56QM5y~OlrZc&w9=)B?nmd@keRn+^= zvm~4sa5987LFDnU{(N|N zJAR8H@}p1fC+H(yTI4n#%~TbImMpuqYn9cQ<0QQ%=PzZItLkC*ef9WJUvfITKWh#D zc#__8`4am9%#NslIUw+<82#SR8AYG|woLfBg#!-&dqq}@P>|I0%lbdy0lSMmNe+}o zj0zZuFr6Wb?Y{Qy-S=|r`bdrDmhnmvkRnkdn`YCleU>Q$=je}LGhh>_QAj6aa_0Oc z%Swsmui;IRx7bN*=AAS@5yW&Y2hy;3&|HAiA8}!HT6!Z!RVn~MZg`RmI6&%#tBZDx zfD+y@Z~NWlk*4l13vmt3AK2wP!fQlnBbECL>?p)F?T)<`w&QN>cP_V>r7UTcsTaaP zTOb$f!P@zf$6>890NVKbIkG8rE?9!Y97sMSZjfF?A zYR8lp`LMoz~O?iaZN;gcX;LC-%Ia*R%A&SLx!YIf29?P+=XAAojK8!^OU*@?R&DK!#G_lsn!#;S375uZ&B0HH1|BO0R90$U>qs zSvHv>H~mAgNCcjo-e+;RjY6B9NCbQrZ|BHjTkehaU<9CSkdd>Vl*ifA2LNOP&R2Qdy3k3-TQ+ zbq=#vI43x`s=%~cGyN&y4Y!FxhwgDe@i6uv8^BLL&3z*SO=D0aLjih?gY4-9uWp5or)H+v~w6n5X#F-I52z=Z_p4JB(;M| zeaVFhuR2|3UD2MzVc~^nSoD2(dD#uL_1PdnIxeA{V5n`#3xf1Zx@4lw(DsQ&H$h zw#%3O<1173hjg2_nhKi!d1ej=h7y`hVjCNB6|HTnx>SWuCE-kgTnfT+YGX4_Lun({ zDv2`>d3vrS)tTf7ps_vvh!Cx^e1BFuWnEAh0(7fkNk|-3oU|iRWdsC6U)?Raft~HN z;^$U}vZK5O8|LV$>6X5T(uYkblv{zwPxnQBh(BQ5tA~J!vGiAMYP^_ki~pkIxDfOZ zUJDwq%O~WueeV6%uN<54&u*c&E4y431cklBNrb06zGOOy4XNT~JS-q(s6@)F@ovbe ze`fial(O4(-su%6@@1+V0MsdLLMyE8;)nou(7}czU(5ASaZYDT(kUZ0L(&g$nF^n9 z9-Pi`ZZLX&)^*M6As4_2Mmc9S7OT)F8KkL2NJ)KJcnCuWU=Wy402A&45#Q9Id~BBH z0cY*xlv!uXzKrXLH!xQu(OtJvEj|0-DmRj1vjFz{c*I4$Pe(+_V|^b~S!0xm{8lq= zZv)@NlcyL3Xdz+*|L137F7y6L-2VsrKw=q^S>F6i%<{Fr8zk06$Ay-(!L$fY@7mcng!2}L0t zgi|KxfB63Xtk_Q8#ZPipQ@!zgjdpEIbK_?q17Hoi4Eiyun$hrc>T(7pOLVLQE=lgGwA+A308p& z7@=09(|$>eLy5gLe{*|3b(M;1n;C^~v?o88jYib48eR4$QGsBFzd}3QuwO^_XE(=B zq+hMi0UFC|dB{LCwch7;zYT=NK})O%sgi0k#yV;My@24^B1+CuZmYOh0^b)5Ba_)) zC%i#_Iev&nsu%I|1N5=MVc#PrlunKAs&hY|3s5;@}`>sB>}gzxuB zB=2vrRyB3uiyW(hkDUNe1@&(b`;>ZvGgw|@s{zVC#_`HXIN_^J@Etb zA7A+F?ot37T{<-vTy8h&b3e+WKHE1oh;pUQrN4yRRrx?mT_9jRa2i4l1fUnLW^Cbl z!I1>VzyFe?VELWWhM?@?t-YPZkD-Qjo@bC2(o#ZtZmr{KZsdFWItV`rs$gp{724@C zL8K5}E0+DHcWcL^{BGei4>@J-3%a#$y6;I}=upc};-NDv-z#kPX26ylOpH)Ov1uU{ zkLj6oiH6l_s+B~_z;|Jc2oi?naS7#3H63~~lWj4rUnd=fCnKdkik<@R&kch9q##G{ z4u!%=rlM~Yp3jk*t8}1B`Sv6<%Z^}~1e@aq zg|JQ`QO2pSjAm-g*?IrNc$^~sIrNBo2$m|Sxanr?Mfs>2@Auu49 zGXlsS<9XS1&8h(dD*Hl&5HBDG!^pJ*lkau_Ur+7`7z;rcs$hT4we?3bT=7Fe<>{5( z2m2(c+hUz2BTHM8dCe*Z3XX&Av;b~a=$6EF>&^E8%nyxO@m_n!q&XD^A{SRjRZQ0L~qDeC=j&0$j6=LNIz@`ni^>ch|sv}^6 zlm>?28yPl@WmDPR?Y-A9X{U9Dv_IsbXJnzKCjkRksLOg#42uG2mE_acbTQ4)J|1V>%U@K(FP3AYhL0U zdeOCPN1qLv!|#c=p!_+%VNV(GHt`RuLRV^vz<5tt-r)yOK**kUWPspVAf|}ZL{LS= z@k(@@!P&W!>wwe`x{+GrFSWhHov7hu?{KuuT%kl#WO@*WX$i_@retlhQBj++SVNCx z5$78LxP>Z=^aJ)D280r_jj=zFfMJFXCIe^B{~V@d1rl_F(qo&AB4bC-vYL>x2jSKX zpuTG-6kgp3e^T&+dtV*i6a~)v@n?n*MffN59y}<0djUX zt27R+SE#hp8bzc#;rk$jw3r4)Q@eI$*`_)=Pvge8@8|8>H3X)<9YX6cXa=ii#Le;(qKm@%0-7$>2ShnYc`j#zJ7gu_FE^?uAkL|H)UIH#gPu^40!6^J=^ zr`}iwa^!4tzW~vOMZAaKF>*8A{^8m$i(VK)>?=#l`xrVe>wseSvM_aF zATNkY>kM_P3?1kE`uIq#mvr-wuTgUH0N<&JhF=(E9%^NS*HLm!4GZ4_XI zL=R5tlG5Mk_1rPfg)sk^llFuKPMPBhuU|L5q#yP_mzxp1o&pAzi-X31sgFpIHn@($ z_>=`AB5(8tP6p2zS5VEvH5J$M` z_much3>S7t3Yo`Yx!>83-hW9LYzDKP?mKdkD#QAK8*M((sx{eBQdrR<^3ZhFP81+& zBnJMUefQyNBji~$5d88Wfw1Lv59aJN9t2!pABLg;ewJ#LXL-10;QcJl+Y4Mtngb)k6JZlCf)3uD_u)J3sYyN;NN5hNbg$%W!i-GK%e&!Us)2IExWSss$YG(hm3kJ-h%yD z>8q^n$+4I(_y_mbT{du4P%h1j3oSpjhY97{+IZ`aA4ug!vNJ6*p?<2H(2w+GD3j$I z1TUXGyNzdf>_yB3grP~FZUs<2Quw;eEi*7s(-MiIkQ%@J^+WGdQvYSUN+TRiD-xto zJ=OUU+kxGYc!HCLNbCvR4lGTp~#L;DFzGd-#gJe*xf(P3hDQz|y)?b9mwU3WUVnpcqXM<@w%r-k*Wr^gzAv)8T^sqA=Ye z!7qy&exJmAcAt~CwS#@yNmjr8*T*!A6w4~E*ibaLRs0CFo(;R3=ODhDt6zWNodmo0 zXx&bT$6&+5c>a|WJ)F4G-^GjY0H#*tY=UNyYr_q5fsrcjk(c^~e*7Lf`!Jd`)p412 zn|^*hV= zFI4UbwA%X@smDd$cQOiMC%jfitTxTb+#`9`G=2rJDfK!E=5ra|So>lc{X1$~w28i+ z4p&cTGwZ#5VueiXS9O8#;RR$yg7tL9!^)Sz&pZYIzlSh}0}V{LxL$Cu%B4U5_}k}- zm~|CsD<076x@<>m=6w6N?WaThIBP`!u{-;WF)xc=2otx*lwf|5+MkdJePjh(B z9SH+%cHGCMAXNxB{_3^otDWdsV7Ob6n{0 z+&!(;iaHOX__5z_$Qk{%xYV%Ig@7iokGBwR`3642ZP#H#v9QGbWl8<|MS*=@qO@Uj z6+SZ_v9`1paUe5tFN~v(b#J3a_Lx0+;r9giZIx-A5TxdbG>xi#AZ5_z1V}B^n)sxT zz49}eK7EWb6wR!6-qQOrHQHkUvshvq%=G2d&@(#XM*Am1;WbnJ{X_!a{ZkphD$^TQ z=Iskb&}=lBm(RHiwJoGg`*NiQ6#RB$T#LF+>#ef;Jne&MxKPX!#r`&TVEFsp2jnNx>dClzpcPy&G&13a_<0qaR3i+k212~hoQ z8nMk{JP-t04I{GW5gUBqcJW-jSMrlw}>p)ptx?WKuCUV77taMiV zHok9V=6yv+Uts@fMY&A}amC=!Yj}eL@=e%XJ#%?agkt1jWF+10{(E9mHLDa>Ll7Vj zG=3cp%ljIB-6pC}6&`xJ*6WCP|IlglLWJ^?yviI8Ve)?V_i4%n;olzny62_`-|IGi z^=}p_O>Z8M;c4|RExu70E7ePW(HWVS&E$+LL6xSQgB`QfMQJ|4pCTFowA39p5P-|$ zUtM_H2HnP8_RoS~Vwk(FhbG zH41licj%=0a;Ln2STFBvU}Ne&O&%8bYKj!h1FA#sNM`232fX|U3QPp#3C?mN2;hE9 z;)!@5ixSPl<89^7gwhHc2YAX1KJK$#*3`KOMIQ253q7-*RJ5k)zp9GBO|Ga~X*^}US5oN@aG&waHV%vi~r{t^`ptTxb zL}q1W8S7*>7oWwvgV4uFLZ(@k`R*=LO_|Gu`prs~!WQXj-NLIa^2(7IHg>BG^N zc|i{-^=&Cek9dkJFQys|sjG9i>LLz|;yCv{^1i%c*h>8zF91kLvS9HBQi~ZU!JL`B zK8N+U0fr1*6??Ium)AF!6tc1eGhXIYL6IRT7rmKp7+>?%5Pa6zC5)KY$ycF0ZJ`G5nEQDG100U-jLkH8^UE4g6wq?sg%pP=-$&G#bcN`^?w3a6 z((s$6eRKcSEIslW-kk5Qi|5Mg-(xdLF}PxxVh$PuO}#aR6pW1kV4Af!Bqh*btXNNZ z>-4(IUl+L4dw+3LcpGut=qB45O+W)Q5?*zZ2A6rJcg`qkSvWA!j^r2mqKuCm6`Py? z@^T#Ux04HemPGd!Hs7NkZdVn1}8_j`o?)*OKZGS!`ff)gF zG?v-lj$wWNWCcw2Mg2o18D~1?3_b0XzdiKBNkYSDpcv@&kp0POmweJE2ZkIQ3B!a! zIgIoE+Xv?;34kyo^QYjZk+tEqZvq^#QG(OzX4~X+KtsoQoddTWUR(yo8R+ObEF1j<-syWOb>)JQ&Zbdu(sctU%Mt zW&YR0{ttY2TTXYZ?~WNU&cES1Z2q(7SrWDh``!J(JM+Nk$!hu&Y;(7E`ZNKTe0w+% zJc?Qnw2B+%UR}0;cB0Rufa(7-3FF}?629@LgTiEC&2uyL6NxexOp?AKT^aAx3gi(W zao>r>MPw0eQ3>IV02uLsC@>yK_epX6GRg4{NEL2wPPF9=*L2RV3yyK8DhuEK>rmmV z`&Q~#c`lgR&93TdOCja|ewOXmPNRh7!&dMT(1ett#iDr8HZW~VqWW@7fe9B6;7S+? zbC`d4@MEau&mKlOPKd>*10q0c{~^baw6!a*w^sY#0Xim{oOsiXiDOhbG&kl3c$$n1 zMRrD83&QucDSEcV*7LIp8VTA@F<%qe+_c`L;6on(>SjAU^}5c9!BCffT>$VQhe=)z z8(=Ej{5>jhmjB3{xDfj2R@VmHQ!CqjlO4KnuOmvHy3K#po$yp_V;p_MKjh1`(rzj6 zHW956k1yvntz{_g?Xbs`avK(IjlTnsu%htO;D7 z?J#x^EzuvVn&NA=!MEj7cwe5A-Z$Zk2LBZH$~%E* zf`((xH0?`}hs|HA%mtwfOEsZJxxrennkTYcwP#FKO5%Lpc^JXhSpV|ZH$Wr;`}`_( zIP==gd3LYyVtwD|*ZJGi{7~x8{=^bGVqu0RJ`n_BZH9+}kz%-4ZRsImi@rx%=ZEKs zcPnUXo6hbJV>fH;@1|bAHIe0ijYI*&kdT|HkDS$9No9 zCHo=*HWb~U+Dtzxr+Esao}6@|;Pf+E$ay0$kQp#s{wlw+7aIKbMdf`OqhoG*;Tco0 zjrP}VQG#Y2cJuqoJg&5({)S(BA}q9T1lGeWRyu=Je|)I!6a+aj!IP^1({)ZYe&x6w zt3a)Dq^TB+A7CdB0-}#z2Ur$W&h3YVw8==!xONy$uQmDWh-@15iEOt!q2m&?ZLA|w z8loSb(0}7y6Xu0?M5Uf4>VZGluB`wMf2oh;m)ghxVda>3m}4%V)r^0nVQ5V6f3>*) z0&VN!N0~GC^P}vj$`EDMZEmVV;N&RISY2C;$0;2(<{Lt&PKzqRByQdiEHGAbwtbS zPj`Da5%U6k1oEtVzI}QNw;!hT6F+~|@=c@$C4NtO@=xgP?|5MyZAyuCzcvq4rdAv@C06%gZ`9%I);R6UGiGJobfux+<0DLS&|MSG4UH z_~o{^^9>ixMg~mY!-@Fai{xaE4^;qy9iZN15Gbn5ZqHWf>Jc5Rv6(#n8`1NcCsdmG zab*dSXVPaE?)wCalD;$ivF%@nB#7D`@YG04p6ed9m}4iJW|pfVMLE<-c{=-8$e?cH zUdU#mCj4gb zZKA^b9p*9S(}8@tw~1RNPHr7tQr;P+-)D8|sq=*o)G%RGqt> zzP5yf`pVxb)I51D_G~Xp^GNK zVI6sAX)a9s)e{8N3?35YA6aQTXuyszK3ah~CemzA&CII#8F&F#KN41~8I^&_%}6MCNb{W87qAF`zj_Y^szhb> z3p3}KbOxotY|(lD=;)`fYE_*{S}x;f^SW#)SU&5X#o|-R|trpa|L5PS5aa0 zTHw8%SDSVtU4?vyrhnq+^@dgFS)|(y{~(4j%3UEiO-rBM9%`)8(dh33pMLiuurNY# z#10AsQ7%*0Cu_DSAU}P;X(JwA64~Q_^R%d_zSm^6Aux?Pn70PM>9EvLeOX z&w9c)pGmcL22;MO3C_B>=NC0RJpMp8?#ZUf=GWRvy z6RHq3B}=MGVg?9@iKFBpsvnkVh3{Vpp=`CcD=u~@ql{my|6?3ssi3mCOPnjI&E}VC zc@X+Yl>;;DNo0W0`0th!X{?luDhOC{E8N=?!w}K1{V=)+1={m(f`Oc|N=07>}3;z{-(A zm{JL=j?Sro5iecmE2-pWlRf(r%|HEQ7kgwQ9+kt=NBhtQI7OwcZ#3%$Uf%^r2nhjY zoQ08MfC%_X{O9~WcirMZMhn#z^ux4Erx-tf-6bHD)9eH&^L>^jvAd^9A^DCDs?0;k zkm7LE*KjP6`2d17MrQaaLqd_Rka}J$csvUec#hw78<=s(hyR>065~YCVCA9+#Q+; za(*L0IEw!r5P|@-;x33L$Lv9 zcuN8YG&g{<(SeJG18~(b!5yywSqQiLAX0;---;}mF5&b4lg|T?LwKREa{9YX_-zL@ZE?Zqi@HxK^2KO1>0LATu{te=T zprmHtY)bDVfxI1S}KBE7V zznP7KQ8HekWU#W6mw`dr-boV}pMQR==&5=Q5T=_q091jfc;R*jX#&=MQ%~@E@9^?`$v48ks<>(fI(F6L(5ppKy|$HWng*bKOb(4|cMUB&z$#ob#XV z5-mg)gmFIybZf=znm3ZPyUO^GJfxt0kmHjaTZ|sthsxXw&}Y)fOUSg=JhRSR^UjZ- zhqqb}Wsyw4zdnj6@#BAJa#-PdI4_dgafFXh85DsEQ_cT+5)XpZq$fZlBA_9UsE9r6 zEFec5?uqN@QhJ^IzwZrwl-5J`CmVPv{(YDTqEqWR^dI;5hXc~cxP%B3v&~s0`Ct89 z@S`i~a^c%V^N81dDT*ItFS*&IN;@O$EgzX0e7x&}TD=!zS}hTpezBLS>mdX(5< z)8DEI(-o_D)c-UX@dA1MuJ*yc>Hf4|`*B2S_O>w*-tbUwtiu`;W(Ud{HTty@(&x(T(F&;M zJ=?H>6`B7nf-90e8V`WSVp|0oEKB-P2M{}4ZDawzvM&a!y>`Y#jCsD%T_l``@ah(I2nJs~Q|%uSKu@k!m~*8B*IoA{*TgtF<(5sHCGG;n@NE%~Xt(G$^&<87u;}Na zx-8cq0g`uA(&RBFo=-4Y1GUZ<``Zw{xL4jfHkZw~%~wvtGueszcXt)_QwH8g!; z%s&3kSa~R$dO$-%L-)c@_hi7&>{6L_M>OZFkUQu;{sL_bUMStNrt{{&O(Wn~*zPOk zB>dnfszb29NSTf2pqIs68k|p-UrSrxgLHqi?3N-UFa!LHy9n1)=s>`yS+J{MEzS@ zNlfGtpma7kG&LR3JE@wB%rFA*h~~KitlO=IP)ZjN6dQLM6qsry zHkB#cyNh#n`)}bCrN1My*;k)^@>e4gJ`LJK?2)Pwp?4Tl4)4FA0(tvY+#1jOUM)xw zlMz4x-f@g^+yKUN`?Vu)|AwujArnM~Pa@y*Q9S8eS(u{-S%(Z5=R~pRl5ZGDjdqH% zC8rW&{##wOpU_oTIG4WXMk4&%2t1;lWcW5&!yxmOT*!hBcKyTqEcNoO+R2;Q?Yj+W z1-Y4?59fijz4(MIDwGe4-baYf08UCs;r|YefD-Md2ST;=cxwpgW=tR76-dQVAhn^= zG9Wk5lQk%jIR@KNU!UMp6@BfU;r+;y4VQ)D2!Il9HX%yW-9nOzV+m$YKzVaO`B8S7t z$!S2Mz`xw>V(RjE`0>bQp<0y&h~Y=M#jpy!#=dE>`=e_AjSZq6u!Dy1xJf~-7|0F! zPR9|n`e_7D2DIV2H(CESQ}hA>U>n|6`%z?YKEA~)BOVY%y=jPV zT=44R!L?J)736X#csn|lfBJ)o8ixaZclguWgrGO<`TN2FMfO}7;5}d+BlK0yTSH3* z4!=;5rOh85&2|x=46hkNaz?)U8&=bcfh=N_#8BNpZ2v$aVBo;sk^*X`v;4-LU;D>! zM*h12MxXIQy)SfAqE4;jY)wgnppazZkdNNVVF;(PLf^qK$FgY9+VFyBKE7UC|f z`R|?&egV11K3s$rJ6!GvoeW=jV*!-e(wA;x(2=d0E_e_%0x--0o8#~m^H1%AH5Z^B zn!TNPn927*bvaf0pt}zhK0o^V@WlGwwKo(*nQ|Q~4_;>~-8y20`HP>@UJa)3nEnGG z5Hwhs|FcmFG16ZVNb5hL`2Gc1{zWIMM{_OiKewV!hCi}U!VuE?s9wU-QbZ!)+Y^tS zGzp5OSi5iq6hmEr$w}&9DFgoB+i*`q`8TBi^MVS{SKEb8Aw%@K7@XCo(De2A`6%mf&a2#~y1N)+kJLD$1HCP!22)(U}xo2|j?WRzt(11j8Z_*v;P$R+Ug*Gy3VxV4K; zGGUGabnW*`Z}~`ydXL-l9e=GC$pY#z|63vy>E*m=$=j}iWP{sRTh0%H54`t>2xYH% zsk+M&u&pNgMCM@3e)Xc?jBWX-TIR_cQ1Z!RW7!B zBjZX=+^3}?SE)B+$EP+0oi1Fp5blDT?*}nsP>filqXH{ms zxU<$hetC`u)Wi+x|EKL-`y^#aQX+sDYIa{M;V%LqLrOk~lR>u0Q!+pyQSU4zY`?E^ z|5@)C)w6G_=i5YYC5SE_u(7hDNYr}uKT|@DSqF%S++lTIbIk^$a>{~0IH8KNFEy%+ zW#$&!ynpgNJh>6uR~?2c)ZMW+h0OKu231(7L_vETPaR+(P)Zy%0~yGm>E9?@@x!Jy z3PYgS}Q@b}x}E#F27@F+j}0=&Ql4gES&f8acMrPAVlVs9$97`FR))R5wI zc&}KFI1UIewh>3PkhnB7u zS3AT8_*|nexznG|Z*DU0c!K@jsI4J)5#DyNi#|e#`l1Vv1`1)*NVcy0LZ``aL0n8B zecupJ(rhq3u8bW0NIRhKYq$v1li+jp*4hfAd&wxYDE8vn1TQ7S@bTM|I2Ob z8vMOIxA7&_j{AKmD+O@EyXT`|dElt0pED^@IV0m)RPBUs*5jW60>>w1!@_G3aBKzG z_f(KfAPBk}-jQtR*Sroq!*3rbQ_m27e+YdzQjUb<_*k8vc_C)y!@cj5E>NxUhPu&g z@Z2<~esU`)ih+4opWe+K7sbN9n*9@n>#@n3*o z?xoROgDuvhq>jJ;Ve{6i<3roQNfgo5^4Q4(|GNExO2Dr7GjgA2zWuKp_K)K0R(6lv z!l$!zW-+T6mb3gQaAFviTQi{|*t%>{(mhTdy+y;Re4qT@kccy#{b z&zWy~kLO@>*WPj2k#H)|7L&gAJ37DmHQAme#@m;(Y8Nu^`D5vf8sZFW#+lA2!HK=( zJ)#hO6JD*`o~&c*&46d}g=Qj@SsoB5ikC z^1V8E+&<-OzuS_C`p5<<(A6fB`LXT(!kV^0_~hL6PpW4={l%|#xgdh?5EIk~lu8{D z2hiyhv3Yxij_#$Wu>P@7SYsl`-~3;}Ktx{34_NL^Kwin&=?!HDv3elQDbcU*qyYpN z(#yw~f1vFGK-t%CC-qa-4FYHbA^h>bag-I&*qaxwn?Qv|idE$<>1H|Gr6JtUu(he2$eg!N z@HTF@dG1)*y;4fxe)4_ZkpaBHH9hXp9p4|gLrRQyuevRd@gSS}JhRnWqrvm|U@>qM z=yl7RQROTKwQtzP3!zUF)_6Ld#NGA6v~2{J9Dd`h6{%+XsU#qGLh%`fB1Hc?wfayK zN`H4BpDp)npVQuu$DVW1qsBS&AJ2eP%6Qw>;k{)Z$8%HL=Q4(a$Ng2_vHw&vA!1L+9zc8vaX2GtqJ{L-;gvF0IR$em zMQ8@{Qp3+3Quk)TJ$?I<8KmwzD*7#(q<@Mc`dchngW}cRG14(Z6K7{T|LhFXwhqUQ;BET;cYqPcAcMgt6M$V9$(?jHo@Sud$an$U&5F zZ1QNh^ztt)E*d#Ij;<43oSKKnd+WNr$_r}+s_O_x6DZSB10*5Q{ourqq>mTl| zx4y^(cy+9;t@R=*j>3_dmm_m)$k$#937V(sllby&5)Xex^UD-|m|q<(jEd#@DV(of zAd7sSdmS*zUDqJ9|K%O2J2OfdUiK{{b{PCy)pi<;hp~7v1CQj&4-10 zgO<3dqhYH1#-Fa}Q{pjql5>>P6gZH21zLfxZ4$SK4T@7b!|`nWF9b*84Bq8&Eht;9 z*P72x&NUCZ7*@B$`FtE=hz5b}S`|c6Ey+j@D1ZibjJaRlR;{cxAWv z?Nqa>QqV*H-*zzaPvpLMHt~nl(x6?vrPpR?zn7~wow?oj*1TKmx4j71>$hvtC$DLD zUrz0^tiP0792U&dxJxNv@r}Elsjn^aSLUu=9#mD{&9n8|ayIL$!H3s>%KEvbchBFW z%cd?VU83mGF#Dar9*s~w&AnmQRQIOvR+uWsuZ?+|a=TzApXO@q^(r%8=}iv#wCnFq z=K9}JbqU@k99Q%j-}NNk+qLCP)jXfmOO|)@?mHcnynd6({mJisP1_}u7k)|eYHXWK z63eQ)E$ufFi!3CWUY2gw%e>omCv}qEX66aH-k&35f9`Q@Us|NPetVqe8=dX*VxJdn ze`q7b=Dn(UA(2sf&g)cOmQFhNJ#<-aMELJZbA#@to>25@kbW<)&!X01 z%NMJt>1ST)tyX)h@?`DxhbgCHr>S4wv}WC&Nw-!{+Z7$2D}74QAcXTvip=M0%Tp_N zor=k`)t|ra^ySr-+(|R9mB(E=`MX#y(wSw)$!iymzB;^c*>%&^*7HxTnRga=soSZT zdDl+9s;r!v8hk6POtzBaig4pRp7eWF(<8gufvNHPu6xs-=e{;mnHzJyGKE+8L0j}; z@%8-e^UCL5HhMiR>sD3Rve&yVZ#{Q1*CO8c+qSr^Z#CN;)(X5>tGG5yUw3<+CfhaL z%bP;hZ?jvgJU67BWyiy74_)6r)_nSxttxn0`0?HE^5(uydHVgP+HE$V?Lv)Leti43 zWA|;f-RqX``95>)^P-fw!Vi{3KNsII-*5f){gdxqd%gVdB1sOBNe=nEW%;i~g_P8J w!5uhoe-Jcg1nPN%MiEAtgE$;km@@t6ukO)1^!cY^83Pb_y85}Sb4q9e0FIsP9{>OV literal 0 HcmV?d00001 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..2165dd02 --- /dev/null +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutter_inappwebview_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.pichillilorenzo.flutterInappwebviewExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.pichillilorenzo. All rights reserved. diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..f7ba7c9b --- /dev/null +++ b/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.print + + + diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..2722837e --- /dev/null +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements new file mode 100644 index 00000000..8ed2e121 --- /dev/null +++ b/example/macos/Runner/Release.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.print + + + diff --git a/ios/Classes/InAppBrowser/InAppBrowserManager.swift b/ios/Classes/InAppBrowser/InAppBrowserManager.swift index 8d77a76a..233686be 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserManager.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserManager.swift @@ -110,7 +110,7 @@ public class InAppBrowserManager: ChannelDelegate { navController.tmpWindow = tmpWindow var animated = true - if let browserOptions = webViewController.browserSettings, browserOptions.hidden { + if let browserSettings = webViewController.browserSettings, browserSettings.hidden { tmpWindow.isHidden = true UIApplication.shared.delegate?.window??.makeKeyAndVisible() animated = false diff --git a/ios/Classes/InAppBrowser/InAppBrowserSettings.swift b/ios/Classes/InAppBrowser/InAppBrowserSettings.swift index ea265651..5bd51f8f 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserSettings.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserSettings.swift @@ -36,7 +36,7 @@ public class InAppBrowserSettings: ISettings { var realOptions: [String: Any?] = toMap() if let inAppBrowserWebViewController = obj { realOptions["hideUrlBar"] = inAppBrowserWebViewController.searchBar.isHidden - realOptions["hideUrlBar"] = inAppBrowserWebViewController.progressBar.isHidden + realOptions["progressBar"] = inAppBrowserWebViewController.progressBar.isHidden realOptions["closeButtonCaption"] = inAppBrowserWebViewController.closeButton.title realOptions["closeButtonColor"] = inAppBrowserWebViewController.closeButton.tintColor?.hexString if let navController = inAppBrowserWebViewController.navigationController { diff --git a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift index 2abbc95d..536eb049 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -254,7 +254,7 @@ public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelega navigationController?.navigationBar.barTintColor = UIColor(hexString: barTintColor) } if let tintColor = browserOptions.toolbarTopTintColor, !tintColor.isEmpty { - navigationController?.navigationBar.barTintColor = UIColor(hexString: tintColor) + navigationController?.navigationBar.tintColor = UIColor(hexString: tintColor) } navigationController?.navigationBar.isTranslucent = browserOptions.toolbarTopTranslucent } diff --git a/lib/src/in_app_webview/in_app_webview.dart b/lib/src/in_app_webview/in_app_webview.dart index fc0ff3b4..f60ea6ec 100755 --- a/lib/src/in_app_webview/in_app_webview.dart +++ b/lib/src/in_app_webview/in_app_webview.dart @@ -688,7 +688,7 @@ class _InAppWebViewState extends State { creationParamsCodec: const StandardMessageCodec(), ); } - } else if (defaultTargetPlatform == TargetPlatform.iOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS/* || defaultTargetPlatform == TargetPlatform.macOS*/) { return UiKitView( viewType: 'com.pichillilorenzo/flutter_inappwebview', onPlatformViewCreated: _onPlatformViewCreated, diff --git a/lib/src/types/print_job_color_mode.dart b/lib/src/types/print_job_color_mode.dart index adbf583f..7edcf5a9 100644 --- a/lib/src/types/print_job_color_mode.dart +++ b/lib/src/types/print_job_color_mode.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; import '../print_job/main.dart'; @@ -9,11 +10,17 @@ part 'print_job_color_mode.g.dart'; class PrintJobColorMode_ { // ignore: unused_field final int _value; + // ignore: unused_field + final dynamic _nativeValue = null; const PrintJobColorMode_._internal(this._value); ///Monochrome color scheme, for example one color is used. + @EnumSupportedPlatforms( + platforms: [EnumAndroidPlatform(value: 1), EnumMacOSPlatform(value: "Gray")]) static const MONOCHROME = const PrintJobColorMode_._internal(1); ///Color color scheme, for example many colors are used. + @EnumSupportedPlatforms( + platforms: [EnumAndroidPlatform(value: 1), EnumMacOSPlatform(value: "RGB")]) static const COLOR = const PrintJobColorMode_._internal(2); } diff --git a/lib/src/types/print_job_color_mode.g.dart b/lib/src/types/print_job_color_mode.g.dart index e6a7212e..83fb130e 100644 --- a/lib/src/types/print_job_color_mode.g.dart +++ b/lib/src/types/print_job_color_mode.g.dart @@ -9,7 +9,7 @@ part of 'print_job_color_mode.dart'; ///Class representing how the printed content of a [PrintJobController] should be laid out. class PrintJobColorMode { final int _value; - final int _nativeValue; + final dynamic _nativeValue; const PrintJobColorMode._internal(this._value, this._nativeValue); // ignore: unused_element factory PrintJobColorMode._internalMultiPlatform( @@ -17,10 +17,38 @@ class PrintJobColorMode { PrintJobColorMode._internal(value, nativeValue()); ///Monochrome color scheme, for example one color is used. - static const MONOCHROME = PrintJobColorMode._internal(1, 1); + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- MacOS + static final MONOCHROME = PrintJobColorMode._internalMultiPlatform(1, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 1; + case TargetPlatform.macOS: + return 'Gray'; + default: + break; + } + return null; + }); ///Color color scheme, for example many colors are used. - static const COLOR = PrintJobColorMode._internal(2, 2); + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- MacOS + static final COLOR = PrintJobColorMode._internalMultiPlatform(2, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 1; + case TargetPlatform.macOS: + return 'RGB'; + default: + break; + } + return null; + }); ///Set of all values of [PrintJobColorMode]. static final Set values = [ @@ -42,7 +70,7 @@ class PrintJobColorMode { } ///Gets a possible [PrintJobColorMode] instance from a native value. - static PrintJobColorMode? fromNativeValue(int? value) { + static PrintJobColorMode? fromNativeValue(dynamic value) { if (value != null) { try { return PrintJobColorMode.values @@ -57,8 +85,8 @@ class PrintJobColorMode { ///Gets [int] value. int toValue() => _value; - ///Gets [int] native value. - int toNativeValue() => _nativeValue; + ///Gets [dynamic] native value. + dynamic toNativeValue() => _nativeValue; @override int get hashCode => _value.hashCode; diff --git a/lib/src/types/print_job_duplex_mode.dart b/lib/src/types/print_job_duplex_mode.dart index 08994d93..724c157b 100644 --- a/lib/src/types/print_job_duplex_mode.dart +++ b/lib/src/types/print_job_duplex_mode.dart @@ -17,18 +17,18 @@ class PrintJobDuplexMode_ { ///No double-sided (duplex) printing; single-sided printing only. @EnumSupportedPlatforms( - platforms: [EnumAndroidPlatform(value: 1), EnumIOSPlatform(value: 0)]) + platforms: [EnumAndroidPlatform(value: 1), EnumIOSPlatform(value: 0), EnumMacOSPlatform(value: 1)]) static const NONE = PrintJobDuplexMode_._internal('NONE'); ///Duplex printing that flips the back page along the long edge of the paper. ///Pages are turned sideways along the long edge - like a book. @EnumSupportedPlatforms( - platforms: [EnumAndroidPlatform(value: 2), EnumIOSPlatform(value: 1)]) + platforms: [EnumAndroidPlatform(value: 2), EnumIOSPlatform(value: 1), EnumMacOSPlatform(value: 2)]) static const LONG_EDGE = PrintJobDuplexMode_._internal('LONG_EDGE'); ///Duplex print that flips the back page along the short edge of the paper. ///Pages are turned upwards along the short edge - like a notepad. @EnumSupportedPlatforms( - platforms: [EnumAndroidPlatform(value: 4), EnumIOSPlatform(value: 2)]) + platforms: [EnumAndroidPlatform(value: 4), EnumIOSPlatform(value: 2), EnumMacOSPlatform(value: 3)]) static const SHORT_EDGE = PrintJobDuplexMode_._internal('SHORT_EDGE'); } diff --git a/lib/src/types/print_job_duplex_mode.g.dart b/lib/src/types/print_job_duplex_mode.g.dart index 043899a5..6202c46b 100644 --- a/lib/src/types/print_job_duplex_mode.g.dart +++ b/lib/src/types/print_job_duplex_mode.g.dart @@ -21,12 +21,15 @@ class PrintJobDuplexMode { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS static final NONE = PrintJobDuplexMode._internalMultiPlatform('NONE', () { switch (defaultTargetPlatform) { case TargetPlatform.android: return 1; case TargetPlatform.iOS: return 0; + case TargetPlatform.macOS: + return 1; default: break; } @@ -39,6 +42,7 @@ class PrintJobDuplexMode { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS static final LONG_EDGE = PrintJobDuplexMode._internalMultiPlatform('LONG_EDGE', () { switch (defaultTargetPlatform) { @@ -46,6 +50,8 @@ class PrintJobDuplexMode { return 2; case TargetPlatform.iOS: return 1; + case TargetPlatform.macOS: + return 2; default: break; } @@ -58,6 +64,7 @@ class PrintJobDuplexMode { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS static final SHORT_EDGE = PrintJobDuplexMode._internalMultiPlatform('SHORT_EDGE', () { switch (defaultTargetPlatform) { @@ -65,6 +72,8 @@ class PrintJobDuplexMode { return 4; case TargetPlatform.iOS: return 2; + case TargetPlatform.macOS: + return 3; default: break; } diff --git a/lib/src/types/print_job_orientation.dart b/lib/src/types/print_job_orientation.dart index 0a754d05..0694737d 100644 --- a/lib/src/types/print_job_orientation.dart +++ b/lib/src/types/print_job_orientation.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; import '../print_job/main.dart'; @@ -12,8 +13,24 @@ class PrintJobOrientation_ { const PrintJobOrientation_._internal(this._value); ///Pages are printed in portrait orientation. + @EnumSupportedPlatforms(platforms: [ + EnumIOSPlatform( + value: 0 + ), + EnumMacOSPlatform( + value: 0 + ) + ]) static const PORTRAIT = const PrintJobOrientation_._internal(0); ///Pages are printed in landscape orientation. + @EnumSupportedPlatforms(platforms: [ + EnumIOSPlatform( + value: 1 + ), + EnumMacOSPlatform( + value: 1 + ) + ]) static const LANDSCAPE = const PrintJobOrientation_._internal(1); } diff --git a/lib/src/types/print_job_orientation.g.dart b/lib/src/types/print_job_orientation.g.dart index c7207c28..3c900d75 100644 --- a/lib/src/types/print_job_orientation.g.dart +++ b/lib/src/types/print_job_orientation.g.dart @@ -17,10 +17,38 @@ class PrintJobOrientation { PrintJobOrientation._internal(value, nativeValue()); ///Pages are printed in portrait orientation. - static const PORTRAIT = PrintJobOrientation._internal(0, 0); + /// + ///**Supported Platforms/Implementations**: + ///- iOS + ///- MacOS + static final PORTRAIT = PrintJobOrientation._internalMultiPlatform(0, () { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return 0; + case TargetPlatform.macOS: + return 0; + default: + break; + } + return null; + }); ///Pages are printed in landscape orientation. - static const LANDSCAPE = PrintJobOrientation._internal(1, 1); + /// + ///**Supported Platforms/Implementations**: + ///- iOS + ///- MacOS + static final LANDSCAPE = PrintJobOrientation._internalMultiPlatform(1, () { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return 1; + case TargetPlatform.macOS: + return 1; + default: + break; + } + return null; + }); ///Set of all values of [PrintJobOrientation]. static final Set values = [ diff --git a/lib/src/types/ssl_certificate.dart b/lib/src/types/ssl_certificate.dart index c045617c..ada6b5af 100644 --- a/lib/src/types/ssl_certificate.dart +++ b/lib/src/types/ssl_certificate.dart @@ -85,10 +85,12 @@ class SslCertificate_ { SslCertificateDName.fromMap(map["issuedBy"]?.cast()), issuedTo: SslCertificateDName.fromMap(map["issuedTo"]?.cast()), - validNotAfterDate: - DateTime.fromMillisecondsSinceEpoch(map["validNotAfterDate"]), - validNotBeforeDate: - DateTime.fromMillisecondsSinceEpoch(map["validNotBeforeDate"]), + validNotAfterDate: map["validNotAfterDate"] != null + ? DateTime.fromMillisecondsSinceEpoch(map["validNotAfterDate"]) + : null, + validNotBeforeDate: map["validNotBeforeDate"] != null + ? DateTime.fromMillisecondsSinceEpoch(map["validNotBeforeDate"]) + : null, x509Certificate: x509Certificate, ); } diff --git a/lib/src/types/ssl_certificate.g.dart b/lib/src/types/ssl_certificate.g.dart index 4d62ab25..0702d486 100644 --- a/lib/src/types/ssl_certificate.g.dart +++ b/lib/src/types/ssl_certificate.g.dart @@ -75,10 +75,12 @@ class SslCertificate { map["issuedBy"]?.cast()), issuedTo: SslCertificateDName.fromMap( map["issuedTo"]?.cast()), - validNotAfterDate: - DateTime.fromMillisecondsSinceEpoch(map["validNotAfterDate"]), - validNotBeforeDate: - DateTime.fromMillisecondsSinceEpoch(map["validNotBeforeDate"]), + validNotAfterDate: map["validNotAfterDate"] != null + ? DateTime.fromMillisecondsSinceEpoch(map["validNotAfterDate"]) + : null, + validNotBeforeDate: map["validNotBeforeDate"] != null + ? DateTime.fromMillisecondsSinceEpoch(map["validNotBeforeDate"]) + : null, x509Certificate: x509Certificate); } diff --git a/macos/Classes/CredentialDatabase.swift b/macos/Classes/CredentialDatabase.swift new file mode 100755 index 00000000..ff555380 --- /dev/null +++ b/macos/Classes/CredentialDatabase.swift @@ -0,0 +1,201 @@ +// +// CredentialDatabase.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 29/10/2019. +// + +import Foundation +import FlutterMacOS + +public class CredentialDatabase: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_credential_database" + static var registrar: FlutterPluginRegistrar? + static var credentialStore: URLCredentialStorage? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: CredentialDatabase.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + CredentialDatabase.registrar = registrar + CredentialDatabase.credentialStore = URLCredentialStorage.shared + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + switch call.method { + case "getAllAuthCredentials": + var allCredentials: [[String: Any?]] = [] + guard let credentialStore = CredentialDatabase.credentialStore else { + result(allCredentials) + return + } + for (protectionSpace, credentials) in credentialStore.allCredentials { + var crendentials: [[String: Any?]] = [] + for c in credentials { + let credential: [String: Any?] = c.value.toMap() + crendentials.append(credential) + } + if crendentials.count > 0 { + let dict: [String : Any] = [ + "protectionSpace": protectionSpace.toMap(), + "credentials": crendentials + ] + allCredentials.append(dict) + } + } + result(allCredentials) + break + case "getHttpAuthCredentials": + var crendentials: [[String: Any?]] = [] + guard let credentialStore = CredentialDatabase.credentialStore else { + result(crendentials) + return + } + + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + + for (protectionSpace, credentials) in credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + for c in credentials { + crendentials.append(c.value.toMap()) + } + break + } + } + result(crendentials) + break + case "setHttpAuthCredential": + guard let credentialStore = CredentialDatabase.credentialStore else { + result(false) + return + } + + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + let username = arguments!["username"] as! String + let password = arguments!["password"] as! String + let credential = URLCredential(user: username, password: password, persistence: .permanent) + credentialStore.set(credential, + for: URLProtectionSpace(host: host, port: urlPort, protocol: urlProtocol, + realm: realm, authenticationMethod: NSURLAuthenticationMethodHTTPBasic)) + result(true) + break + case "removeHttpAuthCredential": + guard let credentialStore = CredentialDatabase.credentialStore else { + result(false) + return + } + + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + let username = arguments!["username"] as! String + let password = arguments!["password"] as! String + + var credential: URLCredential? = nil; + var protectionSpaceCredential: URLProtectionSpace? = nil + + for (protectionSpace, credentials) in credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + for c in credentials { + if c.value.user == username, c.value.password == password { + credential = c.value + protectionSpaceCredential = protectionSpace + break + } + } + } + if credential != nil { + break + } + } + + if let c = credential, let protectionSpace = protectionSpaceCredential { + credentialStore.remove(c, for: protectionSpace) + } + + result(true) + break + case "removeHttpAuthCredentials": + guard let credentialStore = CredentialDatabase.credentialStore else { + result(false) + return + } + + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + + var credentialsToRemove: [URLCredential] = []; + var protectionSpaceCredential: URLProtectionSpace? = nil + + for (protectionSpace, credentials) in credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + protectionSpaceCredential = protectionSpace + for c in credentials { + if let _ = c.value.user, let _ = c.value.password { + credentialsToRemove.append(c.value) + } + } + break + } + } + + if let protectionSpace = protectionSpaceCredential { + for credential in credentialsToRemove { + credentialStore.remove(credential, for: protectionSpace) + } + } + + result(true) + break + case "clearAllAuthCredentials": + guard let credentialStore = CredentialDatabase.credentialStore else { + result(false) + return + } + + for (protectionSpace, credentials) in credentialStore.allCredentials { + for credential in credentials { + credentialStore.remove(credential.value, for: protectionSpace) + } + } + result(true) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public override func dispose() { + super.dispose() + CredentialDatabase.registrar = nil + CredentialDatabase.credentialStore = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift b/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift new file mode 100644 index 00000000..7e60a173 --- /dev/null +++ b/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift @@ -0,0 +1,68 @@ +// +// HeadlessInAppWebView.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/03/21. +// + +import Foundation +import FlutterMacOS + +public class HeadlessInAppWebView : Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_headless_inappwebview_" + var id: String + var channelDelegate: HeadlessWebViewChannelDelegate? + var flutterWebView: FlutterWebViewController? + + public init(id: String, flutterWebView: FlutterWebViewController) { + self.id = id + self.flutterWebView = flutterWebView + let channel = FlutterMethodChannel(name: HeadlessInAppWebView.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + self.channelDelegate = HeadlessWebViewChannelDelegate(headlessWebView: self, channel: channel) + } + + public func onWebViewCreated() { + channelDelegate?.onWebViewCreated(); + } + + public func prepare(params: NSDictionary) { + if let view = flutterWebView?.view() { + view.alphaValue = 0.01 + let initialSize = params["initialSize"] as? [String: Any?] + if let size = Size2D.fromMap(map: initialSize) { + setSize(size: size) + } else { + view.frame = CGRect(x: 0.0, y: 0.0, width: NSApplication.shared.mainWindow?.contentView?.bounds.width ?? 0.0, + height: NSApplication.shared.mainWindow?.contentView?.bounds.height ?? 0.0) + } + } + } + + public func setSize(size: Size2D) { + if let view = flutterWebView?.view() { + let width = size.width == -1.0 ? NSApplication.shared.mainWindow?.contentView?.bounds.width ?? 0.0 : CGFloat(size.width) + let height = size.height == -1.0 ? NSApplication.shared.mainWindow?.contentView?.bounds.height ?? 0.0 : CGFloat(size.height) + view.frame = CGRect(x: 0.0, y: 0.0, width: width, height: height) + } + } + + public func getSize() -> Size2D? { + if let view = flutterWebView?.view() { + return Size2D(width: Double(view.frame.width), height: Double(view.frame.height)) + } + return nil + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + HeadlessInAppWebViewManager.webViews[id] = nil + flutterWebView = nil + } + + deinit { + debugPrint("HeadlessInAppWebView - dealloc") + dispose() + } +} diff --git a/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift b/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift new file mode 100644 index 00000000..92354e8b --- /dev/null +++ b/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift @@ -0,0 +1,68 @@ +// +// HeadlessInAppWebViewManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/2020. +// + +import Foundation + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation + +public class HeadlessInAppWebViewManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_headless_inappwebview" + static var registrar: FlutterPluginRegistrar? + static var webViews: [String: HeadlessInAppWebView?] = [:] + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: HeadlessInAppWebViewManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + HeadlessInAppWebViewManager.registrar = registrar + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + let id: String = arguments!["id"] as! String + + switch call.method { + case "run": + let params = arguments!["params"] as! [String: Any?] + HeadlessInAppWebViewManager.run(id: id, params: params) + result(true) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public static func run(id: String, params: [String: Any?]) { + let flutterWebView = FlutterWebViewController(registrar: HeadlessInAppWebViewManager.registrar!, + withFrame: CGRect.zero, + viewIdentifier: id, + params: params as NSDictionary) + let headlessInAppWebView = HeadlessInAppWebView(id: id, flutterWebView: flutterWebView) + HeadlessInAppWebViewManager.webViews[id] = headlessInAppWebView + + headlessInAppWebView.prepare(params: params as NSDictionary) + headlessInAppWebView.onWebViewCreated() + flutterWebView.makeInitialLoad(params: params as NSDictionary) + } + + public override func dispose() { + super.dispose() + HeadlessInAppWebViewManager.registrar = nil + let headlessWebViews = HeadlessInAppWebViewManager.webViews.values + headlessWebViews.forEach { (headlessWebView: HeadlessInAppWebView?) in + headlessWebView?.dispose() + } + HeadlessInAppWebViewManager.webViews.removeAll() + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift b/macos/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift new file mode 100644 index 00000000..1e6dc9c7 --- /dev/null +++ b/macos/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift @@ -0,0 +1,64 @@ +// +// HeadlessWebViewChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/05/22. +// + +import Foundation +import FlutterMacOS + +public class HeadlessWebViewChannelDelegate : ChannelDelegate { + private weak var headlessWebView: HeadlessInAppWebView? + + public init(headlessWebView: HeadlessInAppWebView, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.headlessWebView = headlessWebView + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "dispose": + if let headlessWebView = headlessWebView { + headlessWebView.dispose() + result(true) + } else { + result(false) + } + break + case "setSize": + if let headlessWebView = headlessWebView { + let sizeMap = arguments!["size"] as? [String: Any?] + if let size = Size2D.fromMap(map: sizeMap) { + headlessWebView.setSize(size: size) + } + result(true) + } else { + result(false) + } + break + case "getSize": + result(headlessWebView?.getSize()?.toMap()) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onWebViewCreated() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onWebViewCreated", arguments: arguments) + } + + public override func dispose() { + super.dispose() + headlessWebView = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/ISettings.swift b/macos/Classes/ISettings.swift new file mode 100755 index 00000000..3c536486 --- /dev/null +++ b/macos/Classes/ISettings.swift @@ -0,0 +1,50 @@ +// +// Options.swift +// flutter_inappwebview +// +// Created by Lorenzo on 26/09/18. +// + +import Foundation + +@objcMembers +public class ISettings: NSObject { + + override init(){ + super.init() + } + + func parse(settings: [String: Any?]) -> ISettings { + for (key, value) in settings { + if !(value is NSNull), value != nil { + if self.responds(to: Selector(key)) { + self.setValue(value, forKey: key) + } else if self.responds(to: Selector("_" + key)) { + self.setValue(value, forKey: "_" + key) + } + } + } + return self + } + + func toMap() -> [String: Any?] { + var settings: [String: Any?] = [:] + var counts = UInt32() + let properties = class_copyPropertyList(object_getClass(self), &counts) + for i in 0.. [String: Any?] { + let realSettings: [String: Any?] = toMap() + return realSettings + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift b/macos/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift new file mode 100644 index 00000000..ca9d4d99 --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift @@ -0,0 +1,29 @@ +// +// InAppBrowserChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/05/22. +// + +import Foundation +import FlutterMacOS + +public class InAppBrowserChannelDelegate : ChannelDelegate { + public override init(channel: FlutterMethodChannel) { + super.init(channel: channel) + } + + public func onBrowserCreated() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onBrowserCreated", arguments: arguments) + } + + public func onExit() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onExit", arguments: arguments) + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserDelegate.swift b/macos/Classes/InAppBrowser/InAppBrowserDelegate.swift new file mode 100644 index 00000000..ba013c58 --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserDelegate.swift @@ -0,0 +1,17 @@ +// +// InAppBrowserDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 14/02/21. +// + +import Foundation + +public protocol InAppBrowserDelegate { + func didChangeTitle(title: String?) + func didStartNavigation(url: URL?) + func didUpdateVisitedHistory(url: URL?) + func didFinishNavigation(url: URL?) + func didFailNavigation(url: URL?, error: Error) + func didChangeProgress(progress: Double) +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserManager.swift b/macos/Classes/InAppBrowser/InAppBrowserManager.swift new file mode 100755 index 00000000..8811fa67 --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserManager.swift @@ -0,0 +1,107 @@ +// +// InAppBrowserManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 18/12/2019. +// + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation + +public class InAppBrowserManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappbrowser" + static let WEBVIEW_STORYBOARD = "WebView" + static let WEBVIEW_STORYBOARD_CONTROLLER_ID = "viewController" + static let NAV_STORYBOARD_CONTROLLER_ID = "navController" + static var registrar: FlutterPluginRegistrar? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: InAppBrowserManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + InAppBrowserManager.registrar = registrar + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "open": + open(arguments: arguments!) + result(true) + break + case "openWithSystemBrowser": + let url = arguments!["url"] as! String + openWithSystemBrowser(url: url, result: result) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func open(arguments: NSDictionary) { + let id = arguments["id"] as! String + let urlRequest = arguments["urlRequest"] as? [String:Any?] + let assetFilePath = arguments["assetFilePath"] as? String + let data = arguments["data"] as? String + let mimeType = arguments["mimeType"] as? String + let encoding = arguments["encoding"] as? String + let baseUrl = arguments["baseUrl"] as? String + let settings = arguments["settings"] as! [String: Any?] + let contextMenu = arguments["contextMenu"] as! [String: Any] + let windowId = arguments["windowId"] as? Int64 + let initialUserScripts = arguments["initialUserScripts"] as? [[String: Any]] + + let browserSettings = InAppBrowserSettings() + let _ = browserSettings.parse(settings: settings) + + let webViewSettings = InAppWebViewSettings() + let _ = webViewSettings.parse(settings: settings) + + let webViewController = InAppBrowserWebViewController() + webViewController.browserSettings = browserSettings + webViewController.webViewSettings = webViewSettings + + webViewController.id = id + webViewController.initialUrlRequest = urlRequest != nil ? URLRequest.init(fromPluginMap: urlRequest!) : nil + webViewController.initialFile = assetFilePath + webViewController.initialData = data + webViewController.initialMimeType = mimeType + webViewController.initialEncoding = encoding + webViewController.initialBaseUrl = baseUrl + webViewController.contextMenu = contextMenu + webViewController.windowId = windowId + webViewController.initialUserScripts = initialUserScripts ?? [] + + let window = InAppBrowserWindow(contentViewController: webViewController) + window.browserSettings = browserSettings + window.contentViewController = webViewController + window.prepare() + + NSApplication.shared.mainWindow?.addChildWindow(window, ordered: .above) + + if browserSettings.hidden { + window.hide() + } + } + + public func openWithSystemBrowser(url: String, result: @escaping FlutterResult) { + let absoluteUrl = URL(string: url)!.absoluteURL + if !NSWorkspace.shared.open(absoluteUrl) { + result(FlutterError(code: "InAppBrowserManager", message: url + " cannot be opened!", details: nil)) + return + } + result(true) + } + + public override func dispose() { + super.dispose() + InAppBrowserManager.registrar = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserSettings.swift b/macos/Classes/InAppBrowser/InAppBrowserSettings.swift new file mode 100755 index 00000000..bc482ec9 --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserSettings.swift @@ -0,0 +1,45 @@ +// +// InAppBrowserOptions.swift +// flutter_inappwebview +// +// Created by Lorenzo on 17/09/18. +// + +import Foundation + +@objcMembers +public class InAppBrowserSettings: ISettings { + + var hidden = false + var hideToolbarTop = true + var toolbarTopBackgroundColor: String? + var hideUrlBar = false + var hideProgressBar = false + + var toolbarTopTranslucent = true + var toolbarTopBarTintColor: String? + var toolbarTopTintColor: String? + var hideToolbarBottom = true + var toolbarBottomBackgroundColor: String? + var toolbarBottomTintColor: String? + var toolbarBottomTranslucent = true + var closeButtonCaption: String? + var closeButtonColor: String? + var presentationStyle = 0 //fullscreen + var transitionStyle = 0 //crossDissolve + + override init(){ + super.init() + } + + override func getRealSettings(obj: InAppBrowserWebViewController?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + if let inAppBrowserWebViewController = obj { + realOptions["hideUrlBar"] = inAppBrowserWebViewController.window?.searchBar?.isHidden + realOptions["progressBar"] = inAppBrowserWebViewController.progressBar.isHidden + realOptions["hideToolbarTop"] = !(inAppBrowserWebViewController.window?.toolbar?.isVisible ?? true) + realOptions["toolbarTopBackgroundColor"] = inAppBrowserWebViewController.window?.backgroundColor + } + return realOptions + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift new file mode 100755 index 00000000..a0982ffa --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -0,0 +1,302 @@ +// +// InAppBrowserWebViewController.swift +// flutter_inappwebview +// +// Created by Lorenzo on 17/09/18. +// + +import FlutterMacOS +import AppKit +import WebKit +import Foundation + +public class InAppBrowserWebViewController: NSViewController, InAppBrowserDelegate, Disposable { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappbrowser_"; + + var progressBar: NSProgressIndicator! + + var window: InAppBrowserWindow? + var id: String = "" + var windowId: Int64? + var webView: InAppWebView? + var channelDelegate: InAppBrowserChannelDelegate? + var initialUrlRequest: URLRequest? + var initialFile: String? + var contextMenu: [String: Any]? + var browserSettings: InAppBrowserSettings? + var webViewSettings: InAppWebViewSettings? + var initialData: String? + var initialMimeType: String? + var initialEncoding: String? + var initialBaseUrl: String? + var initialUserScripts: [[String: Any]] = [] + + public override func loadView() { + let channel = FlutterMethodChannel(name: InAppBrowserWebViewController.METHOD_CHANNEL_NAME_PREFIX + id, binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + channelDelegate = InAppBrowserChannelDelegate(channel: channel) + + var userScripts: [UserScript] = [] + for intialUserScript in initialUserScripts { + userScripts.append(UserScript.fromMap(map: intialUserScript, windowId: windowId)!) + } + + let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(settings: webViewSettings) + if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + webView!.contextMenu = contextMenu + webView!.initialUserScripts = userScripts + } else { + webView = InAppWebView(id: nil, + registrar: nil, + frame: .zero, + configuration: preWebviewConfiguration, + contextMenu: contextMenu, + userScripts: userScripts) + } + + guard let webView = webView else { + return + } + + webView.inAppBrowserDelegate = self + webView.id = id + webView.channelDelegate = WebViewChannelDelegate(webView: webView, channel: channel) + + prepareWebView() + webView.windowCreated = true + + progressBar = NSProgressIndicator() + progressBar.style = .bar + progressBar.isIndeterminate = false + progressBar.startAnimation(self) + + view = NSView(frame: NSApplication.shared.mainWindow?.frame ?? .zero) + view.addSubview(webView) + view.addSubview(progressBar, positioned: .above, relativeTo: webView) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + webView?.translatesAutoresizingMaskIntoConstraints = false + progressBar.translatesAutoresizingMaskIntoConstraints = false + + webView?.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0).isActive = true + webView?.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0).isActive = true + webView?.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0.0).isActive = true + webView?.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0.0).isActive = true + + progressBar.topAnchor.constraint(equalTo: self.view.topAnchor, constant: -6.0).isActive = true + progressBar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0.0).isActive = true + progressBar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0.0).isActive = true + + if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView?.load(webViewTransport.request) + channelDelegate?.onBrowserCreated() + } else { + if #available(macOS 10.13, *) { + if let contentBlockers = webView?.settings?.contentBlockers, contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: String.Encoding.utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + + if let error = error { + print(error.localizedDescription) + return + } + + let configuration = self.webView!.configuration + configuration.userContentController.add(contentRuleList!) + + self.initLoad() + } + return + } catch { + print(error.localizedDescription) + } + } + } + + initLoad() + } + } + + public override func viewDidAppear() { + super.viewDidAppear() + window = view.window as? InAppBrowserWindow + } + + public func initLoad() { + if let initialFile = initialFile { + do { + try webView?.loadFile(assetFilePath: initialFile) + } + catch let error as NSError { + dump(error) + } + } + else if let initialData = initialData { + let baseUrl = URL(string: initialBaseUrl ?? "about:blank")! + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView?.settings?.allowingReadAccessTo, baseUrl.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView?.loadData(data: initialData, mimeType: initialMimeType!, encoding: initialEncoding!, baseUrl: baseUrl, allowingReadAccessTo: allowingReadAccessToURL) + } + else if let initialUrlRequest = initialUrlRequest { + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView?.settings?.allowingReadAccessTo, let url = initialUrlRequest.url, url.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView?.loadUrl(urlRequest: initialUrlRequest, allowingReadAccessTo: allowingReadAccessToURL) + } + + channelDelegate?.onBrowserCreated() + } + + public func prepareWebView() { + webView?.settings = webViewSettings + webView?.prepare() + + if let browserSettings = browserSettings { + if browserSettings.hideProgressBar { + progressBar.isHidden = true + } + } + } + + public func didChangeTitle(title: String?) { + guard let title = title else { + return + } + window?.title = title + window?.update() + } + + public func didStartNavigation(url: URL?) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + progressBar.doubleValue = 0.0 + progressBar.isHidden = false + guard let url = url else { + return + } + window?.searchBar?.stringValue = url.absoluteString + } + + public func didUpdateVisitedHistory(url: URL?) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + guard let url = url else { + return + } + window?.searchBar?.stringValue = url.absoluteString + } + + public func didFinishNavigation(url: URL?) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + progressBar.doubleValue = 0.0 + progressBar.isHidden = true + guard let url = url else { + return + } + window?.searchBar?.stringValue = url.absoluteString + } + + public func didFailNavigation(url: URL?, error: Error) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + progressBar.doubleValue = 0.0 + progressBar.isHidden = true + } + + public func didChangeProgress(progress: Double) { + progressBar.isHidden = false + progressBar.doubleValue = progress * 100 + if progress == 100.0 { + progressBar.isHidden = true + } + } + + @objc public func reload() { + webView?.reload() + didUpdateVisitedHistory(url: webView?.url) + } + + @objc public func goBack() { + if let webView = webView, webView.canGoBack { + webView.goBack() + } + } + + @objc public func goForward() { + if let webView = webView, webView.canGoForward { + webView.goForward() + } + } + + @objc public func goBackOrForward(steps: Int) { + webView?.goBackOrForward(steps: steps) + } + + public func setSettings(newSettings: InAppBrowserSettings, newSettingsMap: [String: Any]) { + window?.setSettings(newSettings: newSettings, newSettingsMap: newSettingsMap) + + let newInAppWebViewSettings = InAppWebViewSettings() + let _ = newInAppWebViewSettings.parse(settings: newSettingsMap) + webView?.setSettings(newSettings: newInAppWebViewSettings, newSettingsMap: newSettingsMap) + + if newSettingsMap["hideProgressBar"] != nil, browserSettings?.hideProgressBar != newSettings.hideProgressBar { + progressBar.isHidden = newSettings.hideProgressBar + } + + browserSettings = newSettings + webViewSettings = newInAppWebViewSettings + } + + public func getSettings() -> [String: Any?]? { + let webViewSettingsMap = webView?.getSettings() + if (self.browserSettings == nil || webViewSettingsMap == nil) { + return nil + } + var settingsMap = self.browserSettings!.getRealSettings(obj: self) + settingsMap.merge(webViewSettingsMap!, uniquingKeysWith: { (current, _) in current }) + return settingsMap + } + + public func hide() { + window?.hide() + } + + public func show() { + window?.show() + } + + public func close() { + window?.close() + } + + public func dispose() { + channelDelegate?.onExit() + channelDelegate?.dispose() + channelDelegate = nil + webView?.dispose() + webView = nil + window = nil + } + + deinit { + debugPrint("InAppBrowserWebViewController - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserWindow.swift b/macos/Classes/InAppBrowser/InAppBrowserWindow.swift new file mode 100644 index 00000000..61baf46a --- /dev/null +++ b/macos/Classes/InAppBrowser/InAppBrowserWindow.swift @@ -0,0 +1,276 @@ +// +// InAppBrowserNavigationController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 14/02/21. +// + +import Foundation + +struct ToolbarIdentifiers { + static let searchBar = NSToolbarItem.Identifier(rawValue: "SearchBar") + static let backButton = NSToolbarItem.Identifier(rawValue: "BackButton") + static let forwardButton = NSToolbarItem.Identifier(rawValue: "ForwardButton") + static let reloadButton = NSToolbarItem.Identifier(rawValue: "ReloadButton") +} + +public class InAppBrowserWindow : NSWindow, NSWindowDelegate, NSToolbarDelegate, NSSearchFieldDelegate { + var searchItem: NSToolbarItem? + var backItem: NSToolbarItem? + var forwardItem: NSToolbarItem? + var reloadItem: NSToolbarItem? + + var reloadButton: NSButton? { + get { + return reloadItem?.view as? NSButton + } + } + var backButton: NSButton? { + get { + return backItem?.view as? NSButton + } + } + var forwardButton: NSButton? { + get { + return forwardItem?.view as? NSButton + } + } + var searchBar: NSSearchField? { + get { + if #available(macOS 11.0, *), let searchItem = searchItem as? NSSearchToolbarItem { + return searchItem.searchField + } else { + return searchItem?.view as? NSSearchField + } + } + } + + var browserSettings: InAppBrowserSettings? + + public func prepare() { + title = "" + delegate = self + + if #available(macOS 10.13, *) { + let windowToolbar = NSToolbar() + windowToolbar.delegate = self + if #available(macOS 11.0, *) { + searchItem = NSSearchToolbarItem(itemIdentifier: ToolbarIdentifiers.searchBar) + (searchItem as! NSSearchToolbarItem).searchField.delegate = self + toolbarStyle = .expanded + } else { + searchItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.searchBar) + let textField = NSSearchField() + textField.usesSingleLineMode = true + textField.delegate = self + searchItem?.view = textField + } + searchItem?.label = "" + windowToolbar.displayMode = .default + + backItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.backButton) + backItem?.label = "" + if let webViewController = contentViewController as? InAppBrowserWebViewController { + if #available(macOS 11.0, *) { + backItem?.view = NSButton(image: NSImage(systemSymbolName: "chevron.left", + accessibilityDescription: "Go Back")!, + target: webViewController, + action: #selector(InAppBrowserWebViewController.goBack)) + } else { + backItem?.view = NSButton(title: "\u{2039}", + target: webViewController, + action: #selector(InAppBrowserWebViewController.goBack)) + } + } + + forwardItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.forwardButton) + forwardItem?.label = "" + if let webViewController = contentViewController as? InAppBrowserWebViewController { + if #available(macOS 11.0, *) { + forwardItem?.view = NSButton(image: NSImage(systemSymbolName: "chevron.right", + accessibilityDescription: "Go Forward")!, + target: webViewController, + action: #selector(InAppBrowserWebViewController.goForward)) + } else { + forwardItem?.view = NSButton(title: "\u{203A}", + target: webViewController, + action: #selector(InAppBrowserWebViewController.goForward)) + } + } + + reloadItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.reloadButton) + reloadItem?.label = "" + if let webViewController = contentViewController as? InAppBrowserWebViewController { + if #available(macOS 11.0, *) { + reloadItem?.view = NSButton(image: NSImage(systemSymbolName: "arrow.counterclockwise", + accessibilityDescription: "Reload")!, + target: webViewController, + action: #selector(InAppBrowserWebViewController.reload)) + } else { + reloadItem?.view = NSButton(title: "Reload", + target: webViewController, + action: #selector(InAppBrowserWebViewController.reload)) + } + } + + if #available(macOS 10.14, *) { + windowToolbar.centeredItemIdentifier = ToolbarIdentifiers.searchBar + } + toolbar = windowToolbar + } + + forwardButton?.isEnabled = false + backButton?.isEnabled = false + + if let browserSettings = browserSettings { + if !browserSettings.hideToolbarTop { + toolbar?.isVisible = true + if browserSettings.hideUrlBar { + if #available(macOS 11.0, *) { + (searchItem as! NSSearchToolbarItem).searchField.isHidden = true + } else { + searchItem?.view?.isHidden = true + } + } + if let bgColor = browserSettings.toolbarTopBackgroundColor, !bgColor.isEmpty { + backgroundColor = NSColor(hexString: bgColor) + } + } + else { + toolbar?.isVisible = false + } + } + } + + public func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [ ToolbarIdentifiers.searchBar, + ToolbarIdentifiers.backButton, + ToolbarIdentifiers.forwardButton, + ToolbarIdentifiers.reloadButton, + .flexibleSpace ] + } + + public func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.flexibleSpace, + ToolbarIdentifiers.searchBar, + .flexibleSpace, + ToolbarIdentifiers.reloadButton, + ToolbarIdentifiers.backButton, + ToolbarIdentifiers.forwardButton] + } + + public func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + switch(itemIdentifier) { + case ToolbarIdentifiers.searchBar: + return searchItem + case ToolbarIdentifiers.backButton: + return backItem + case ToolbarIdentifiers.forwardButton: + return forwardItem + case ToolbarIdentifiers.reloadButton: + return reloadItem + default: + return nil + } + } + + public func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + if (commandSelector == #selector(NSResponder.insertNewline(_:))) { + // ENTER key + var searchField: NSSearchField? = nil + if #available(macOS 11.0, *), let searchBar = searchItem as? NSSearchToolbarItem { + searchField = searchBar.searchField + } else if let searchBar = searchItem { + searchField = searchBar.view as? NSSearchField + } + + guard let searchField, + let urlEncoded = searchField.stringValue.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: urlEncoded) else { + return false + } + + let request = URLRequest(url: url) + (contentViewController as? InAppBrowserWebViewController)?.webView?.load(request) + + return true + } + + return false + } + + public func hide() { + orderOut(self) + + } + + public func show() { + if !(NSApplication.shared.mainWindow?.childWindows?.contains(self) ?? false) { + NSApplication.shared.mainWindow?.addChildWindow(self, ordered: .above) + } else { + orderFront(self) + } + NSApplication.shared.activate(ignoringOtherApps: true) + } + + public func setSettings(newSettings: InAppBrowserSettings, newSettingsMap: [String: Any]) { + if newSettingsMap["hidden"] != nil, browserSettings?.hidden != newSettings.hidden { + if newSettings.hidden { + hide() + } + else { + show() + } + } + + if newSettingsMap["hideUrlBar"] != nil, browserSettings?.hideUrlBar != newSettings.hideUrlBar { + searchBar?.isHidden = newSettings.hideUrlBar + } + + if newSettingsMap["hideToolbarTop"] != nil, browserSettings?.hideToolbarTop != newSettings.hideToolbarTop { + toolbar?.isVisible = !newSettings.hideToolbarTop + } + + if newSettingsMap["toolbarTopBackgroundColor"] != nil, browserSettings?.toolbarTopBackgroundColor != newSettings.toolbarTopBackgroundColor { + if let bgColor = newSettings.toolbarTopBackgroundColor, !bgColor.isEmpty { + backgroundColor = NSColor(hexString: bgColor) + } else { + backgroundColor = nil + } + } + + browserSettings = newSettings + } + + public func windowWillClose(_ notification: Notification) { + dispose() + } + + public func dispose() { + delegate = nil + if let webViewController = contentViewController as? InAppBrowserWebViewController { + webViewController.dispose() + } + if #available(macOS 11.0, *) { + (searchItem as? NSSearchToolbarItem)?.searchField.delegate = nil + } else { + (searchItem?.view as? NSTextField)?.delegate = nil + searchItem?.view = nil + } + searchItem = nil + (backItem?.view as? NSButton)?.target = nil + backItem?.view = nil + backItem = nil + (forwardItem?.view as? NSButton)?.target = nil + forwardItem?.view = nil + forwardItem = nil + (reloadItem?.view as? NSButton)?.target = nil + reloadItem?.view = nil + reloadItem = nil + } + + deinit { + debugPrint("InAppBrowserWindow - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppWebView/ContextMenuSettings.swift b/macos/Classes/InAppWebView/ContextMenuSettings.swift new file mode 100644 index 00000000..a1df58d9 --- /dev/null +++ b/macos/Classes/InAppWebView/ContextMenuSettings.swift @@ -0,0 +1,17 @@ +// +// ContextMenuOptions.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 30/05/2020. +// + +import Foundation + +public class ContextMenuSettings: ISettings { + + var hideDefaultSystemContextMenuItems = false; + + override init(){ + super.init() + } +} diff --git a/macos/Classes/InAppWebView/CustomSchemeHandler.swift b/macos/Classes/InAppWebView/CustomSchemeHandler.swift new file mode 100755 index 00000000..95400fb2 --- /dev/null +++ b/macos/Classes/InAppWebView/CustomSchemeHandler.swift @@ -0,0 +1,45 @@ +// +// CustomeSchemeHandler.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 25/10/2019. +// + +import FlutterMacOS +import Foundation +import WebKit + +@available(macOS 10.13, *) +public class CustomSchemeHandler : NSObject, WKURLSchemeHandler { + var schemeHandlers: [Int:WKURLSchemeTask] = [:] + + public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + schemeHandlers[urlSchemeTask.hash] = urlSchemeTask + let inAppWebView = webView as! InAppWebView + let request = WebResourceRequest.init(fromURLRequest: urlSchemeTask.request) + let callback = WebViewChannelDelegate.LoadResourceWithCustomSchemeCallback() + callback.nonNullSuccess = { (response: CustomSchemeResponse) in + if (self.schemeHandlers[urlSchemeTask.hash] != nil) { + let urlResponse = URLResponse(url: request.url, mimeType: response.contentType, expectedContentLength: -1, textEncodingName: response.contentEncoding) + urlSchemeTask.didReceive(urlResponse) + urlSchemeTask.didReceive(response.data) + urlSchemeTask.didFinish() + self.schemeHandlers.removeValue(forKey: urlSchemeTask.hash) + } + return false + } + callback.error = { (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + } + + if let channelDelegate = inAppWebView.channelDelegate { + channelDelegate.onLoadResourceWithCustomScheme(request: request, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + public func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { + schemeHandlers.removeValue(forKey: urlSchemeTask.hash) + } +} diff --git a/macos/Classes/InAppWebView/FlutterWebViewController.swift b/macos/Classes/InAppWebView/FlutterWebViewController.swift new file mode 100755 index 00000000..e4b7e702 --- /dev/null +++ b/macos/Classes/InAppWebView/FlutterWebViewController.swift @@ -0,0 +1,179 @@ +// +// FlutterWebViewController.swift +// flutter_inappwebview +// +// Created by Lorenzo on 13/11/18. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class FlutterWebViewController: NSObject, /*FlutterPlatformView,*/ Disposable { + + var myView: NSView? + + init(registrar: FlutterPluginRegistrar, withFrame frame: CGRect, viewIdentifier viewId: Any, params: NSDictionary) { + super.init() + + myView = NSView(frame: frame) + + let initialSettings = params["initialSettings"] as! [String: Any?] + let contextMenu = params["contextMenu"] as? [String: Any] + let windowId = params["windowId"] as? Int64 + let initialUserScripts = params["initialUserScripts"] as? [[String: Any]] + + var userScripts: [UserScript] = [] + if let initialUserScripts = initialUserScripts { + for intialUserScript in initialUserScripts { + userScripts.append(UserScript.fromMap(map: intialUserScript, windowId: windowId)!) + } + } + + let settings = InAppWebViewSettings() + let _ = settings.parse(settings: initialSettings) + let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(settings: settings) + + var webView: InAppWebView? + + if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + webView!.id = viewId + let channel = FlutterMethodChannel(name: InAppWebView.METHOD_CHANNEL_NAME_PREFIX + String(describing: viewId), + binaryMessenger: registrar.messenger) + webView!.channelDelegate = WebViewChannelDelegate(webView: webView!, channel: channel) + webView!.frame = myView!.bounds + webView!.contextMenu = contextMenu + webView!.initialUserScripts = userScripts + } else { + webView = InAppWebView(id: viewId, + registrar: registrar, + frame: myView!.bounds, + configuration: preWebviewConfiguration, + contextMenu: contextMenu, + userScripts: userScripts) + } + + webView!.autoresizingMask = [.width, .height] + myView!.autoresizesSubviews = true + myView!.autoresizingMask = [.width, .height] + myView!.addSubview(webView!) + + webView!.settings = settings + webView!.prepare() + webView!.windowCreated = true + } + + public func webView() -> InAppWebView? { + for subview in myView?.subviews ?? [] + { + if let item = subview as? InAppWebView + { + return item + } + } + return nil + } + + public func view() -> NSView { + return myView! + } + + public func makeInitialLoad(params: NSDictionary) { + guard let webView = webView() else { + return + } + + let windowId = params["windowId"] as? Int64 + let initialUrlRequest = params["initialUrlRequest"] as? [String: Any?] + let initialFile = params["initialFile"] as? String + let initialData = params["initialData"] as? [String: String?] + + if windowId == nil { + if #available(macOS 10.13, *) { + webView.configuration.userContentController.removeAllContentRuleLists() + if let contentBlockers = webView.settings?.contentBlockers, contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: String.Encoding.utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + + if let error = error { + print(error.localizedDescription) + return + } + + let configuration = webView.configuration + configuration.userContentController.add(contentRuleList!) + + self.load(initialUrlRequest: initialUrlRequest, initialFile: initialFile, initialData: initialData) + } + return + } catch { + print(error.localizedDescription) + } + } + } + load(initialUrlRequest: initialUrlRequest, initialFile: initialFile, initialData: initialData) + } + else if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView.load(webViewTransport.request) + } + } + + func load(initialUrlRequest: [String:Any?]?, initialFile: String?, initialData: [String: String?]?) { + guard let webView = webView() else { + return + } + + if let initialFile = initialFile { + do { + try webView.loadFile(assetFilePath: initialFile) + } + catch let error as NSError { + dump(error) + } + } + else if let initialData = initialData, let data = initialData["data"]!, + let mimeType = initialData["mimeType"]!, let encoding = initialData["encoding"]!, + let baseUrl = URL(string: initialData["baseUrl"]! ?? "about:blank") { + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView.settings?.allowingReadAccessTo, baseUrl.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView.loadData(data: data, + mimeType: mimeType, + encoding: encoding, + baseUrl: baseUrl, + allowingReadAccessTo: allowingReadAccessToURL) + } + else if let initialUrlRequest = initialUrlRequest { + let urlRequest = URLRequest.init(fromPluginMap: initialUrlRequest) + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView.settings?.allowingReadAccessTo, let url = urlRequest.url, url.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView.loadUrl(urlRequest: urlRequest, allowingReadAccessTo: allowingReadAccessToURL) + } + } + + public func dispose() { + if let webView = webView() { + webView.dispose() + } + myView = nil + } + + deinit { + debugPrint("FlutterWebViewController - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppWebView/FlutterWebViewFactory.swift b/macos/Classes/InAppWebView/FlutterWebViewFactory.swift new file mode 100755 index 00000000..223a8c81 --- /dev/null +++ b/macos/Classes/InAppWebView/FlutterWebViewFactory.swift @@ -0,0 +1,35 @@ +// +// FlutterWebViewFactory.swift +// flutter_inappwebview +// +// Created by Lorenzo on 13/11/18. +// + +import SwiftUI +import Cocoa +import FlutterMacOS +import Foundation + +public class FlutterWebViewFactory: NSObject, FlutterPlatformViewFactory { + static let VIEW_TYPE_ID = "com.pichillilorenzo/flutter_inappwebview" + private var registrar: FlutterPluginRegistrar? + + init(registrar: FlutterPluginRegistrar?) { + super.init() + self.registrar = registrar + } + + public func createArgsCodec() -> (FlutterMessageCodec & NSObjectProtocol)? { + return FlutterStandardMessageCodec.sharedInstance() + } + + public func create(withViewIdentifier viewId: Int64, arguments args: Any?) -> NSView { + let arguments = args as? NSDictionary + let webviewController = FlutterWebViewController(registrar: registrar!, + withFrame: .zero, + viewIdentifier: viewId, + params: arguments!) + webviewController.makeInitialLoad(params: arguments!) + return webviewController.view() + } +} diff --git a/macos/Classes/InAppWebView/InAppWebView.swift b/macos/Classes/InAppWebView/InAppWebView.swift new file mode 100755 index 00000000..c8894aad --- /dev/null +++ b/macos/Classes/InAppWebView/InAppWebView.swift @@ -0,0 +1,2485 @@ +// +// InAppWebView.swift +// flutter_inappwebview +// +// Created by Lorenzo on 21/10/18. +// + +import FlutterMacOS +import Foundation +import WebKit + +public class InAppWebView: WKWebView, WKUIDelegate, + WKNavigationDelegate, WKScriptMessageHandler, + WKDownloadDelegate, + Disposable { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_" + + var id: Any? // viewId + var windowId: Int64? + var windowCreated = false + var inAppBrowserDelegate: InAppBrowserDelegate? + var channelDelegate: WebViewChannelDelegate? + var settings: InAppWebViewSettings? + var webMessageChannels: [String:WebMessageChannel] = [:] + var webMessageListeners: [WebMessageListener] = [] + var currentOriginalUrl: URL? + var inFullscreen = false + private var printJobCompletionHandler: PrintJobController.CompletionHandler? + + static var sslCertificatesMap: [String: SslCertificate] = [:] // [URL host name : SslCertificate] + static var credentialsProposed: [URLCredential] = [] + + var lastScrollX: CGFloat = 0 + var lastScrollY: CGFloat = 0 + + // Used to manage pauseTimers() and resumeTimers() + var isPausedTimers = false + var isPausedTimersCompletionHandler: (() -> Void)? + + var contextMenu: [String: Any]? + var initialUserScripts: [UserScript] = [] + + var lastLongPressTouchPoint: CGPoint? + + var lastTouchPoint: CGPoint? + var lastTouchPointTimestamp = Int64(Date().timeIntervalSince1970 * 1000) + + var contextMenuIsShowing = false + // flag used for the workaround to trigger onCreateContextMenu event as the same on Android + var onCreateContextMenuEventTriggeredWhenMenuDisabled = false + + var customIMPs: [IMP] = [] + + static var windowWebViews: [Int64:WebViewTransport] = [:] + static var windowAutoincrementId: Int64 = 0; + + var callAsyncJavaScriptBelowIOS14Results: [String:((Any?) -> Void)] = [:] + + var oldZoomScale = Float(1.0) + + init(id: Any?, registrar: FlutterPluginRegistrar?, frame: CGRect, configuration: WKWebViewConfiguration, + contextMenu: [String: Any]?, userScripts: [UserScript] = []) { + super.init(frame: frame, configuration: configuration) + self.id = id + if let id = id, let registrar = registrar { + let channel = FlutterMethodChannel(name: InAppWebView.METHOD_CHANNEL_NAME_PREFIX + String(describing: id), + binaryMessenger: registrar.messenger) + self.channelDelegate = WebViewChannelDelegate(webView: self, channel: channel) + } + self.contextMenu = contextMenu + self.initialUserScripts = userScripts + uiDelegate = self + navigationDelegate = self + } + + required public init(coder aDecoder: NSCoder) { + super.init(coder: aDecoder)! + } + + public func prepare() { + addObserver(self, + forKeyPath: #keyPath(WKWebView.estimatedProgress), + options: .new, + context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.url), + options: [.new, .old], + context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.title), + options: [.new, .old], + context: nil) + + if #available(macOS 12.0, *) { + addObserver(self, + forKeyPath: #keyPath(WKWebView.cameraCaptureState), + options: [.new, .old], + context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.microphoneCaptureState), + options: [.new, .old], + context: nil) + } + + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// addObserver(self, +// forKeyPath: #keyPath(WKWebView.fullscreenState), +// options: .new, +// context: nil) +// } else { + // listen for videos playing in fullscreen + NotificationCenter.default.addObserver(self, + selector: #selector(onEnterFullscreen(_:)), + name: NSWindow.didEnterFullScreenNotification, + object: window) + + // listen for videos stopping to play in fullscreen + NotificationCenter.default.addObserver(self, + selector: #selector(onExitFullscreen(_:)), + name: NSWindow.didExitFullScreenNotification, + object: window) +// } + + if let settings = settings { + + if #available(macOS 12.0, *), settings.transparentBackground { + underPageBackgroundColor = .clear + } + + allowsBackForwardNavigationGestures = settings.allowsBackForwardNavigationGestures + if #available(iOS 9.0, *) { + allowsLinkPreview = settings.allowsLinkPreview + if !settings.userAgent.isEmpty { + customUserAgent = settings.userAgent + } + } + + if #available(macOS 11.0, *) { + mediaType = settings.mediaType + pageZoom = CGFloat(settings.pageZoom) + } + + if #available(macOS 12.0, *) { + if let underPageBackgroundColor = settings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty { + self.underPageBackgroundColor = NSColor(hexString: underPageBackgroundColor) + } + } + + // debugging is always enabled for iOS, + // there isn't any option to set about it such as on Android. + + if settings.clearCache { + clearCache() + } + } + + prepareAndAddUserScripts() + + if windowId != nil { + // The new created window webview has the same WKWebViewConfiguration variable reference. + // So, we cannot set another WKWebViewConfiguration for it unfortunately! + // This is a limitation of the official WebKit API. + return + } + + configuration.preferences = WKPreferences() + if let settings = settings { + configuration.allowsAirPlayForMediaPlayback = settings.allowsAirPlayForMediaPlayback + configuration.preferences.javaScriptCanOpenWindowsAutomatically = settings.javaScriptCanOpenWindowsAutomatically + configuration.preferences.minimumFontSize = CGFloat(settings.minimumFontSize) + + if #available(macOS 10.15, *) { + configuration.preferences.isFraudulentWebsiteWarningEnabled = settings.isFraudulentWebsiteWarningEnabled + configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: settings.preferredContentMode)! + } + + configuration.preferences.javaScriptEnabled = settings.javaScriptEnabled + if #available(macOS 11.0, *) { + configuration.defaultWebpagePreferences.allowsContentJavaScript = settings.javaScriptEnabled + } + + if #available(macOS 11.3, *) { + configuration.preferences.isTextInteractionEnabled = settings.isTextInteractionEnabled + } + + if #available(macOS 12.0, *) { + configuration.preferences.isSiteSpecificQuirksModeEnabled = settings.isSiteSpecificQuirksModeEnabled + configuration.preferences.isElementFullscreenEnabled = settings.isElementFullscreenEnabled + } + } + } + + public func prepareAndAddUserScripts() -> Void { + if windowId != nil { + // The new created window webview has the same WKWebViewConfiguration variable reference. + // So, we cannot set another WKWebViewConfiguration for it unfortunately! + // This is a limitation of the official WebKit API. + return + } + configuration.userContentController = WKUserContentController() + configuration.userContentController.initialize() + + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + return + } + + configuration.userContentController.addPluginScript(PROMISE_POLYFILL_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(CONSOLE_LOG_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(PRINT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT) + if let settings = settings { + if settings.useShouldInterceptAjaxRequest { + configuration.userContentController.addPluginScript(INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT) + } + if settings.useShouldInterceptFetchRequest { + configuration.userContentController.addPluginScript(INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT) + } + if settings.useOnLoadResource { + configuration.userContentController.addPluginScript(ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT) + } + if !settings.supportZoom { + configuration.userContentController.addPluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + } else if settings.enableViewportScale { + configuration.userContentController.addPluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) + } + } + configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.add(self, name: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessagePortMessageReceived") + configuration.userContentController.add(self, name: "onWebMessagePortMessageReceived") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessageListenerPostMessageReceived") + configuration.userContentController.add(self, name: "onWebMessageListenerPostMessageReceived") + configuration.userContentController.addUserOnlyScripts(initialUserScripts) + configuration.userContentController.sync(scriptMessageHandler: self) + } + + public static func preWKWebViewConfiguration(settings: InAppWebViewSettings?) -> WKWebViewConfiguration { + let configuration = WKWebViewConfiguration() + + configuration.processPool = WKProcessPoolManager.sharedProcessPool + + if let settings = settings { + configuration.suppressesIncrementalRendering = settings.suppressesIncrementalRendering + + if settings.allowUniversalAccessFromFileURLs { + configuration.setValue(settings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs") + } + + if settings.allowFileAccessFromFileURLs { + configuration.preferences.setValue(settings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs") + } + + if settings.incognito { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } else if settings.cacheEnabled { + configuration.websiteDataStore = WKWebsiteDataStore.default() + } + if !settings.applicationNameForUserAgent.isEmpty { + if let applicationNameForUserAgent = configuration.applicationNameForUserAgent { + configuration.applicationNameForUserAgent = applicationNameForUserAgent + " " + settings.applicationNameForUserAgent + } + } + + if #available(macOS 10.12, *) { + configuration.mediaTypesRequiringUserActionForPlayback = settings.mediaPlaybackRequiresUserGesture ? .all : [] + } + + if #available(macOS 10.13, *) { + for scheme in settings.resourceCustomSchemes { + configuration.setURLSchemeHandler(CustomSchemeHandler(), forURLScheme: scheme) + } + if settings.sharedCookiesEnabled { + // More info to sending cookies with WKWebView + // https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303 + // Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies + // See also https://forums.developer.apple.com/thread/97194 + // check if websiteDataStore has not been initialized before + if(!settings.incognito && !settings.cacheEnabled) { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } + for cookie in HTTPCookieStorage.shared.cookies ?? [] { + configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: nil) + } + } + } + + if #available(macOS 11.0, *) { + configuration.limitsNavigationsToAppBoundDomains = settings.limitsNavigationsToAppBoundDomains + } + + if #available(macOS 11.3, *) { + configuration.upgradeKnownHostsToHTTPS = settings.upgradeKnownHostsToHTTPS + } + } + + return configuration + } + + @objc func onCreateContextMenu() { + let mapSorted = SharedLastTouchPointTimestamp.sorted { $0.value > $1.value } + if (mapSorted.first?.key != self) { + return + } + + contextMenuIsShowing = true + + let hitTestResult = HitTestResult(type: .unknownType, extra: nil) + + if let lastLongPressTouhLocation = lastLongPressTouchPoint { + if configuration.preferences.javaScriptEnabled { + self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(lastLongPressTouhLocation.x),\(lastLongPressTouhLocation.y))", completionHandler: {(value, error) in + if error != nil { + print("Long press gesture recognizer error: \(error?.localizedDescription ?? "")") + } else if let value = value as? [String: Any?] { + self.channelDelegate?.onCreateContextMenu(hitTestResult: HitTestResult.fromMap(map: value) ?? hitTestResult) + } else { + self.channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult) + } + }) + } else { + channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult) + } + } else { + channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult) + } + } + + @objc func onHideContextMenu() { + if contextMenuIsShowing == false { + return + } + contextMenuIsShowing = false + channelDelegate?.onHideContextMenu() + } + + override public func observeValue(forKeyPath keyPath: String?, of object: Any?, + change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == #keyPath(WKWebView.estimatedProgress) { + initializeWindowIdJS() + let progress = Int(estimatedProgress * 100) + channelDelegate?.onProgressChanged(progress: progress) + inAppBrowserDelegate?.didChangeProgress(progress: estimatedProgress) + } else if keyPath == #keyPath(WKWebView.url) && change?[.newKey] is URL { + initializeWindowIdJS() + let newUrl = change?[NSKeyValueChangeKey.newKey] as? URL + channelDelegate?.onUpdateVisitedHistory(url: newUrl?.absoluteString, isReload: nil) + inAppBrowserDelegate?.didUpdateVisitedHistory(url: newUrl) + } else if keyPath == #keyPath(WKWebView.title) && change?[.newKey] is String { + let newTitle = change?[.newKey] as? String + channelDelegate?.onTitleChanged(title: newTitle) + inAppBrowserDelegate?.didChangeTitle(title: newTitle) + } + else if #available(macOS 12.0, *) { + if keyPath == #keyPath(WKWebView.cameraCaptureState) || keyPath == #keyPath(WKWebView.microphoneCaptureState) { + var oldState: WKMediaCaptureState? = nil + if let oldValue = change?[.oldKey] as? Int { + oldState = WKMediaCaptureState.init(rawValue: oldValue) + } + var newState: WKMediaCaptureState? = nil + if let newValue = change?[.newKey] as? Int { + newState = WKMediaCaptureState.init(rawValue: newValue) + } + if oldState != newState { + if keyPath == #keyPath(WKWebView.cameraCaptureState) { + channelDelegate?.onCameraCaptureStateChanged(oldState: oldState, newState: newState) + } else { + channelDelegate?.onMicrophoneCaptureStateChanged(oldState: oldState, newState: newState) + } + } + } + } else if #available(iOS 16.0, *) { + // TODO: Still not working on iOS 16.0! +// if keyPath == #keyPath(WKWebView.fullscreenState) { +// if fullscreenState == .enteringFullscreen { +// channelDelegate?.onEnterFullscreen() +// } else if fullscreenState == .exitingFullscreen { +// channelDelegate?.onExitFullscreen() +// } +// } + } + } + + public func initializeWindowIdJS() { + if let windowId = windowId { + if #available(macOS 11.0, *) { + let contentWorlds = configuration.userContentController.getContentWorlds(with: windowId) + for contentWorld in contentWorlds { + let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId)) + evaluateJavascript(source: source, contentWorld: contentWorld) + } + } else { + let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId)) + evaluateJavascript(source: source) + } + } + } + + public func goBackOrForward(steps: Int) { + if canGoBackOrForward(steps: steps) { + if (steps > 0) { + let index = steps - 1 + go(to: self.backForwardList.forwardList[index]) + } + else if (steps < 0){ + let backListLength = self.backForwardList.backList.count + let index = backListLength + steps + go(to: self.backForwardList.backList[index]) + } + } + } + + public func canGoBackOrForward(steps: Int) -> Bool { + let currentIndex = self.backForwardList.backList.count + return (steps >= 0) + ? steps <= self.backForwardList.forwardList.count + : currentIndex + steps >= 0 + } + + @available(macOS 10.13, *) + public func takeScreenshot (with: [String: Any?]?, completionHandler: @escaping (_ screenshot: Data?) -> Void) { + var snapshotConfiguration: WKSnapshotConfiguration? = nil + if let with = with { + snapshotConfiguration = WKSnapshotConfiguration() + if let rect = with["rect"] as? [String: Double] { + snapshotConfiguration!.rect = CGRect.fromMap(map: rect) + } + if let snapshotWidth = with["snapshotWidth"] as? Double { + snapshotConfiguration!.snapshotWidth = NSNumber(value: snapshotWidth) + } + if #available(macOS 10.15, *), let afterScreenUpdates = with["afterScreenUpdates"] as? Bool { + snapshotConfiguration!.afterScreenUpdates = afterScreenUpdates + } + } + takeSnapshot(with: snapshotConfiguration, completionHandler: {(image, error) -> Void in + var imageData: Data? = nil + if let screenshot = image, let cgImage = screenshot.cgImage(forProposedRect: nil, context: nil, hints: nil) { + let newRep = NSBitmapImageRep(cgImage: cgImage) + if let with = with { + switch with["compressFormat"] as! String { + case "JPEG": + let quality = Float(with["quality"] as! Int) / 100 + imageData = newRep.representation(using: .jpeg, properties: [ + NSBitmapImageRep.PropertyKey.compressionFactor:quality]) + break + case "PNG": + imageData = newRep.representation(using: .png, properties: [:]) + break + default: + imageData = newRep.representation(using: .png, properties: [:]) + } + } + else { + imageData = newRep.representation(using: .png, properties: [:]) + } + } + completionHandler(imageData) + }) + } + + @available(macOS 11.0, *) + public func createPdf (configuration: [String: Any?]?, completionHandler: @escaping (_ pdf: Data?) -> Void) { + let pdfConfiguration: WKPDFConfiguration = .init() + if let configuration = configuration { + if let rect = configuration["rect"] as? [String: Double] { + pdfConfiguration.rect = CGRect.fromMap(map: rect) + } + } + createPDF(configuration: pdfConfiguration) { (result) in + switch (result) { + case .success(let data): + completionHandler(data) + return + case .failure(let error): + print(error.localizedDescription) + completionHandler(nil) + return + } + } + } + + @available(macOS 11.0, *) + public func createWebArchiveData (dataCompletionHandler: @escaping (_ webArchiveData: Data?) -> Void) { + createWebArchiveData(completionHandler: { (result) in + switch (result) { + case .success(let data): + dataCompletionHandler(data) + return + case .failure(let error): + print(error.localizedDescription) + dataCompletionHandler(nil) + return + } + }) + } + + @available(macOS 11.0, *) + public func saveWebArchive (filePath: String, autoname: Bool, completionHandler: @escaping (_ path: String?) -> Void) { + createWebArchiveData(dataCompletionHandler: { (webArchiveData) in + if let webArchiveData = webArchiveData { + var localUrl = URL(fileURLWithPath: filePath) + if autoname { + if let url = self.url { + // tries to mimic Android saveWebArchive method + let invalidCharacters = CharacterSet(charactersIn: "\\/:*?\"<>|") + .union(.newlines) + .union(.illegalCharacters) + .union(.controlCharacters) + + let currentPageUrlFileName = url.path + .components(separatedBy: invalidCharacters) + .joined(separator: "") + + let fullPath = filePath + "/" + currentPageUrlFileName + ".webarchive" + localUrl = URL(fileURLWithPath: fullPath) + } else { + completionHandler(nil) + return + } + } + do { + try webArchiveData.write(to: localUrl) + completionHandler(localUrl.path) + } catch { + // Catch any errors + print(error.localizedDescription) + completionHandler(nil) + } + } else { + completionHandler(nil) + } + }) + } + + public func loadUrl(urlRequest: URLRequest, allowingReadAccessTo: URL?) { + let url = urlRequest.url! + + if #available(iOS 9.0, *), let allowingReadAccessTo = allowingReadAccessTo, url.scheme == "file", allowingReadAccessTo.scheme == "file" { + loadFileURL(url, allowingReadAccessTo: allowingReadAccessTo) + } else { + load(urlRequest) + } + } + + public func postUrl(url: URL, postData: Data) { + var request = URLRequest(url: url) + + request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + request.httpBody = postData + load(request) + } + + public func loadData(data: String, mimeType: String, encoding: String, baseUrl: URL, allowingReadAccessTo: URL?) { + if #available(iOS 9.0, *), let allowingReadAccessTo = allowingReadAccessTo, baseUrl.scheme == "file", allowingReadAccessTo.scheme == "file" { + loadFileURL(baseUrl, allowingReadAccessTo: allowingReadAccessTo) + } + + if #available(iOS 9.0, *) { + load(data.data(using: .utf8)!, mimeType: mimeType, characterEncodingName: encoding, baseURL: baseUrl) + } else { + loadHTMLString(data, baseURL: baseUrl) + } + } + + public func loadFile(assetFilePath: String) throws { + let assetURL = try Util.getUrlAsset(assetFilePath: assetFilePath) + let urlRequest = URLRequest(url: assetURL) + loadUrl(urlRequest: urlRequest, allowingReadAccessTo: nil) + } + + func setSettings(newSettings: InAppWebViewSettings, newSettingsMap: [String: Any]) { + + // MUST be the first! In this way, all the settings that uses evaluateJavaScript can be applied/blocked! + if #available(iOS 13.0, *) { + if newSettingsMap["applePayAPIEnabled"] != nil && settings?.applePayAPIEnabled != newSettings.applePayAPIEnabled { + if let settings = settings { + settings.applePayAPIEnabled = newSettings.applePayAPIEnabled + } + if !newSettings.applePayAPIEnabled { + // re-add WKUserScripts for the next page load + prepareAndAddUserScripts() + } else { + configuration.userContentController.removeAllUserScripts() + } + } + } + + if (newSettingsMap["incognito"] != nil && settings?.incognito != newSettings.incognito && newSettings.incognito) { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } else if (newSettingsMap["cacheEnabled"] != nil && settings?.cacheEnabled != newSettings.cacheEnabled && newSettings.cacheEnabled) { + configuration.websiteDataStore = WKWebsiteDataStore.default() + } + + if #available(macOS 10.13, *) { + if (newSettingsMap["sharedCookiesEnabled"] != nil && settings?.sharedCookiesEnabled != newSettings.sharedCookiesEnabled && newSettings.sharedCookiesEnabled) { + if(!newSettings.incognito && !newSettings.cacheEnabled) { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } + for cookie in HTTPCookieStorage.shared.cookies ?? [] { + configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: nil) + } + } + } + + if newSettingsMap["enableViewportScale"] != nil && settings?.enableViewportScale != newSettings.enableViewportScale { + if !newSettings.enableViewportScale { + if configuration.userContentController.userScripts.contains(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) { + configuration.userContentController.removePluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) + evaluateJavaScript(NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE) + } + } else { + evaluateJavaScript(ENABLE_VIEWPORT_SCALE_JS_SOURCE) + configuration.userContentController.addUserScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) + } + } + + if newSettingsMap["supportZoom"] != nil && settings?.supportZoom != newSettings.supportZoom { + if newSettings.supportZoom { + if configuration.userContentController.userScripts.contains(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) { + configuration.userContentController.removePluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + evaluateJavaScript(SUPPORT_ZOOM_JS_SOURCE) + } + } else { + evaluateJavaScript(NOT_SUPPORT_ZOOM_JS_SOURCE) + configuration.userContentController.addUserScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + } + } + + if newSettingsMap["useOnLoadResource"] != nil && settings?.useOnLoadResource != newSettings.useOnLoadResource { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE, + enable: newSettings.useOnLoadResource, + pluginScript: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT) + } else { + newSettings.useOnLoadResource = false + } + } + + if newSettingsMap["useShouldInterceptAjaxRequest"] != nil && settings?.useShouldInterceptAjaxRequest != newSettings.useShouldInterceptAjaxRequest { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE, + enable: newSettings.useShouldInterceptAjaxRequest, + pluginScript: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT) + } else { + newSettings.useShouldInterceptFetchRequest = false + } + } + + if newSettingsMap["useShouldInterceptFetchRequest"] != nil && settings?.useShouldInterceptFetchRequest != newSettings.useShouldInterceptFetchRequest { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE, + enable: newSettings.useShouldInterceptFetchRequest, + pluginScript: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT) + } else { + newSettings.useShouldInterceptFetchRequest = false + } + } + + if newSettingsMap["mediaPlaybackRequiresUserGesture"] != nil && settings?.mediaPlaybackRequiresUserGesture != newSettings.mediaPlaybackRequiresUserGesture { + if #available(macOS 10.12, *) { + configuration.mediaTypesRequiringUserActionForPlayback = (newSettings.mediaPlaybackRequiresUserGesture) ? .all : [] + } + } + + if newSettingsMap["suppressesIncrementalRendering"] != nil && settings?.suppressesIncrementalRendering != newSettings.suppressesIncrementalRendering { + configuration.suppressesIncrementalRendering = newSettings.suppressesIncrementalRendering + } + + if newSettingsMap["allowsBackForwardNavigationGestures"] != nil && settings?.allowsBackForwardNavigationGestures != newSettings.allowsBackForwardNavigationGestures { + allowsBackForwardNavigationGestures = newSettings.allowsBackForwardNavigationGestures + } + + if newSettingsMap["javaScriptCanOpenWindowsAutomatically"] != nil && settings?.javaScriptCanOpenWindowsAutomatically != newSettings.javaScriptCanOpenWindowsAutomatically { + configuration.preferences.javaScriptCanOpenWindowsAutomatically = newSettings.javaScriptCanOpenWindowsAutomatically + } + + if newSettingsMap["minimumFontSize"] != nil && settings?.minimumFontSize != newSettings.minimumFontSize { + configuration.preferences.minimumFontSize = CGFloat(newSettings.minimumFontSize) + } + + if #available(macOS 10.15, *) { + if newSettingsMap["isFraudulentWebsiteWarningEnabled"] != nil && settings?.isFraudulentWebsiteWarningEnabled != newSettings.isFraudulentWebsiteWarningEnabled { + configuration.preferences.isFraudulentWebsiteWarningEnabled = newSettings.isFraudulentWebsiteWarningEnabled + } + if newSettingsMap["preferredContentMode"] != nil && settings?.preferredContentMode != newSettings.preferredContentMode { + configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: newSettings.preferredContentMode)! + } + } + + if newSettingsMap["allowsLinkPreview"] != nil && settings?.allowsLinkPreview != newSettings.allowsLinkPreview { + allowsLinkPreview = newSettings.allowsLinkPreview + } + if newSettingsMap["allowsAirPlayForMediaPlayback"] != nil && settings?.allowsAirPlayForMediaPlayback != newSettings.allowsAirPlayForMediaPlayback { + configuration.allowsAirPlayForMediaPlayback = newSettings.allowsAirPlayForMediaPlayback + } + if newSettingsMap["applicationNameForUserAgent"] != nil && settings?.applicationNameForUserAgent != newSettings.applicationNameForUserAgent && newSettings.applicationNameForUserAgent != "" { + configuration.applicationNameForUserAgent = newSettings.applicationNameForUserAgent + } + if newSettingsMap["userAgent"] != nil && settings?.userAgent != newSettings.userAgent && newSettings.userAgent != "" { + customUserAgent = newSettings.userAgent + } + + if newSettingsMap["allowUniversalAccessFromFileURLs"] != nil && settings?.allowUniversalAccessFromFileURLs != newSettings.allowUniversalAccessFromFileURLs { + configuration.setValue(newSettings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs") + } + + if newSettingsMap["allowFileAccessFromFileURLs"] != nil && settings?.allowFileAccessFromFileURLs != newSettings.allowFileAccessFromFileURLs { + configuration.preferences.setValue(newSettings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs") + } + + if newSettingsMap["clearCache"] != nil && newSettings.clearCache { + clearCache() + } + + if newSettingsMap["javaScriptEnabled"] != nil && settings?.javaScriptEnabled != newSettings.javaScriptEnabled { + configuration.preferences.javaScriptEnabled = newSettings.javaScriptEnabled + } + + if #available(macOS 11.0, *) { + if settings?.mediaType != newSettings.mediaType { + mediaType = newSettings.mediaType + } + + if newSettingsMap["pageZoom"] != nil && settings?.pageZoom != newSettings.pageZoom { + pageZoom = CGFloat(newSettings.pageZoom) + } + + if newSettingsMap["limitsNavigationsToAppBoundDomains"] != nil && settings?.limitsNavigationsToAppBoundDomains != newSettings.limitsNavigationsToAppBoundDomains { + configuration.limitsNavigationsToAppBoundDomains = newSettings.limitsNavigationsToAppBoundDomains + } + + if newSettingsMap["javaScriptEnabled"] != nil && settings?.javaScriptEnabled != newSettings.javaScriptEnabled { + configuration.defaultWebpagePreferences.allowsContentJavaScript = newSettings.javaScriptEnabled + } + } + + if #available(macOS 10.13, *), newSettingsMap["contentBlockers"] != nil { + configuration.userContentController.removeAllContentRuleLists() + let contentBlockers = newSettings.contentBlockers + if contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: String.Encoding.utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + if let error = error { + print(error.localizedDescription) + return + } + self.configuration.userContentController.add(contentRuleList!) + } + } catch { + print(error.localizedDescription) + } + } + } + + if #available(macOS 11.3, *) { + if newSettingsMap["upgradeKnownHostsToHTTPS"] != nil && settings?.upgradeKnownHostsToHTTPS != newSettings.upgradeKnownHostsToHTTPS { + configuration.upgradeKnownHostsToHTTPS = newSettings.upgradeKnownHostsToHTTPS + } + if newSettingsMap["isTextInteractionEnabled"] != nil && settings?.isTextInteractionEnabled != newSettings.isTextInteractionEnabled { + configuration.preferences.isTextInteractionEnabled = newSettings.isTextInteractionEnabled + } + } + + if #available(macOS 12.0, *) { + if newSettingsMap["underPageBackgroundColor"] != nil, settings?.underPageBackgroundColor != newSettings.underPageBackgroundColor, + let underPageBackgroundColor = newSettings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty { + self.underPageBackgroundColor = NSColor(hexString: underPageBackgroundColor) + } + } + if #available(macOS 12.0, *) { + if newSettingsMap["isSiteSpecificQuirksModeEnabled"] != nil, settings?.isSiteSpecificQuirksModeEnabled != newSettings.isSiteSpecificQuirksModeEnabled { + configuration.preferences.isSiteSpecificQuirksModeEnabled = newSettings.isSiteSpecificQuirksModeEnabled + } + } + + self.settings = newSettings + } + + func getSettings() -> [String: Any?]? { + if (self.settings == nil) { + return nil + } + return self.settings!.getRealSettings(obj: self) + } + + public func enablePluginScriptAtRuntime(flagVariable: String, enable: Bool, pluginScript: PluginScript) { + evaluateJavascript(source: flagVariable) { (alreadyLoaded) in + if let alreadyLoaded = alreadyLoaded as? Bool, alreadyLoaded { + let enableSource = "\(flagVariable) = \(enable);" + if #available(macOS 11.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in self.configuration.userContentController.contentWorlds { + self.evaluateJavaScript(enableSource, frame: nil, contentWorld: contentWorld, completionHandler: nil) + } + } else { + self.evaluateJavaScript(enableSource, completionHandler: nil) + } + if !enable { + self.configuration.userContentController.removePluginScripts(with: pluginScript.groupName!) + } + } + else if enable { + if #available(macOS 11.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in self.configuration.userContentController.contentWorlds { + self.evaluateJavaScript(pluginScript.source, frame: nil, contentWorld: contentWorld, completionHandler: nil) + self.configuration.userContentController.addPluginScript(pluginScript) + } + } else { + self.evaluateJavaScript(pluginScript.source, completionHandler: nil) + self.configuration.userContentController.addPluginScript(pluginScript) + } + self.configuration.userContentController.sync(scriptMessageHandler: self) + } + } + } + + public func clearCache() { + let date = NSDate(timeIntervalSince1970: 0) + WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: date as Date, completionHandler:{ }) + } + + public func injectDeferredObject(source: String, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) { + var jsToInject = source + if let wrapper = jsWrapper { + let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: []) + let sourceArrayString = String(data: jsonData!, encoding: String.Encoding.utf8) + let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2)) + jsToInject = String(format: wrapper, sourceString!) + } + + evaluateJavaScript(jsToInject) { (value, error) in + guard let completionHandler = completionHandler else { + return + } + + if let error = error { + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + } + + if value == nil { + completionHandler(nil) + return + } + + completionHandler(value) + } + } + + @available(macOS 11.0, *) + public func injectDeferredObject(source: String, contentWorld: WKContentWorld, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) { + var jsToInject = source + if let wrapper = jsWrapper { + let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: []) + let sourceArrayString = String(data: jsonData!, encoding: String.Encoding.utf8) + let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2)) + jsToInject = String(format: wrapper, sourceString!) + } + + jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: jsToInject, contentWorld: contentWorld) + + evaluateJavaScript(jsToInject, frame: nil, contentWorld: contentWorld) { (evalResult) in + guard let completionHandler = completionHandler else { + return + } + + switch (evalResult) { + case .success(let value): + completionHandler(value) + return + case .failure(let error): + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + break + } + + completionHandler(nil) + } + } + + public override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + super.evaluateJavaScript(javaScriptString, completionHandler: completionHandler) + } + + @available(macOS 11.0, *) + public func evaluateJavaScript(_ javaScript: String, frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + return + } + super.evaluateJavaScript(javaScript, in: frame, in: contentWorld, completionHandler: completionHandler) + } + + public func evaluateJavascript(source: String, completionHandler: ((Any?) -> Void)? = nil) { + injectDeferredObject(source: source, withWrapper: nil, completionHandler: completionHandler) + } + + @available(macOS 11.0, *) + public func evaluateJavascript(source: String, contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) { + injectDeferredObject(source: source, contentWorld: contentWorld, withWrapper: nil, completionHandler: completionHandler) + } + + @available(macOS 11.0, *) + public func callAsyncJavaScript(_ functionBody: String, arguments: [String : Any] = [:], frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + return + } + super.callAsyncJavaScript(functionBody, arguments: arguments, in: frame, in: contentWorld, completionHandler: completionHandler) + } + + @available(macOS 11.0, *) + public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) { + let jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: functionBody, contentWorld: contentWorld) + + callAsyncJavaScript(jsToInject, arguments: arguments, frame: nil, contentWorld: contentWorld) { (evalResult) in + guard let completionHandler = completionHandler else { + return + } + + var body: [String: Any?] = [ + "value": nil, + "error": nil + ] + + switch (evalResult) { + case .success(let value): + body["value"] = value + break + case .failure(let error): + let userInfo = (error as NSError).userInfo + body["error"] = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: body["error"]), messageLevel: 3) + break + } + + completionHandler(body) + } + } + + public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], completionHandler: ((Any?) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + completionHandler?(nil) + } + + var jsToInject = functionBody + + let resultUuid = NSUUID().uuidString + if let completionHandler = completionHandler { + callAsyncJavaScriptBelowIOS14Results[resultUuid] = completionHandler + } + + var functionArgumentNamesList: [String] = [] + var functionArgumentValuesList: [String] = [] + let keys = arguments.keys + keys.forEach { (key) in + functionArgumentNamesList.append(key) + functionArgumentValuesList.append("obj.\(key)") + } + + let functionArgumentNames = functionArgumentNamesList.joined(separator: ", ") + let functionArgumentValues = functionArgumentValuesList.joined(separator: ", ") + + jsToInject = CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES, with: functionArgumentNames) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES, with: functionArgumentValues) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ, with: Util.JSONStringify(value: arguments)) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_BODY, with: jsToInject) + .replacingOccurrences(of: PluginScriptsUtil.VAR_RESULT_UUID, with: resultUuid) + + evaluateJavaScript(jsToInject) { (value, error) in + if let error = error { + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + completionHandler?(nil) + self.callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid) + } + } + } + + public func injectJavascriptFileFromUrl(urlFile: String, scriptHtmlTagAttributes: [String:Any?]?) { + var scriptAttributes = "" + if let scriptHtmlTagAttributes = scriptHtmlTagAttributes { + if let typeAttr = scriptHtmlTagAttributes["type"] as? String { + scriptAttributes += " script.type = '\(typeAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let idAttr = scriptHtmlTagAttributes["id"] as? String { + let scriptIdEscaped = idAttr.replacingOccurrences(of: "\'", with: "\\'") + scriptAttributes += " script.id = '\(scriptIdEscaped)'; " + scriptAttributes += """ + script.onload = function() { + if (window.\(JAVASCRIPT_BRIDGE_NAME) != null) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onInjectedScriptLoaded', '\(scriptIdEscaped)'); + } + }; + """ + scriptAttributes += """ + script.onerror = function() { + if (window.\(JAVASCRIPT_BRIDGE_NAME) != null) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onInjectedScriptError', '\(scriptIdEscaped)'); + } + }; + """ + } + if let asyncAttr = scriptHtmlTagAttributes["async"] as? Bool, asyncAttr { + scriptAttributes += " script.async = true; " + } + if let deferAttr = scriptHtmlTagAttributes["defer"] as? Bool, deferAttr { + scriptAttributes += " script.defer = true; " + } + if let crossOriginAttr = scriptHtmlTagAttributes["crossOrigin"] as? String { + scriptAttributes += " script.crossOrigin = '\(crossOriginAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let integrityAttr = scriptHtmlTagAttributes["integrity"] as? String { + scriptAttributes += " script.integrity = '\(integrityAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let noModuleAttr = scriptHtmlTagAttributes["noModule"] as? Bool, noModuleAttr { + scriptAttributes += " script.noModule = true; " + } + if let nonceAttr = scriptHtmlTagAttributes["nonce"] as? String { + scriptAttributes += " script.nonce = '\(nonceAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let referrerPolicyAttr = scriptHtmlTagAttributes["referrerPolicy"] as? String { + scriptAttributes += " script.referrerPolicy = '\(referrerPolicyAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + } + let jsWrapper = "(function(d) { var script = d.createElement('script'); \(scriptAttributes) script.src = %@; d.body.appendChild(script); })(document);" + injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil) + } + + public func injectCSSCode(source: String) { + let jsWrapper = "(function(d) { var style = d.createElement('style'); style.innerHTML = %@; d.head.appendChild(style); })(document);" + injectDeferredObject(source: source, withWrapper: jsWrapper, completionHandler: nil) + } + + public func injectCSSFileFromUrl(urlFile: String, cssLinkHtmlTagAttributes: [String:Any?]?) { + var cssLinkAttributes = "" + var alternateStylesheet = "" + if let cssLinkHtmlTagAttributes = cssLinkHtmlTagAttributes { + if let idAttr = cssLinkHtmlTagAttributes["id"] as? String { + cssLinkAttributes += " link.id = '\(idAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let mediaAttr = cssLinkHtmlTagAttributes["media"] as? String { + cssLinkAttributes += " link.media = '\(mediaAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let crossOriginAttr = cssLinkHtmlTagAttributes["crossOrigin"] as? String { + cssLinkAttributes += " link.crossOrigin = '\(crossOriginAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let integrityAttr = cssLinkHtmlTagAttributes["integrity"] as? String { + cssLinkAttributes += " link.integrity = '\(integrityAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let referrerPolicyAttr = cssLinkHtmlTagAttributes["referrerPolicy"] as? String { + cssLinkAttributes += " link.referrerPolicy = '\(referrerPolicyAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let disabledAttr = cssLinkHtmlTagAttributes["disabled"] as? Bool, disabledAttr { + cssLinkAttributes += " link.disabled = true; " + } + if let alternateAttr = cssLinkHtmlTagAttributes["alternate"] as? Bool, alternateAttr { + alternateStylesheet = "alternate " + } + if let titleAttr = cssLinkHtmlTagAttributes["title"] as? String { + cssLinkAttributes += " link.title = '\(titleAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + } + let jsWrapper = "(function(d) { var link = d.createElement('link'); link.rel='\(alternateStylesheet)stylesheet', link.type='text/css'; \(cssLinkAttributes) link.href = %@; d.head.appendChild(link); })(document);" + injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil) + } + + public func getCopyBackForwardList() -> [String: Any] { + let currentList = backForwardList + let currentIndex = currentList.backList.count + var completeList = currentList.backList + if currentList.currentItem != nil { + completeList.append(currentList.currentItem!) + } + completeList.append(contentsOf: currentList.forwardList) + + var history: [[String: String]] = [] + + for historyItem in completeList { + var historyItemMap: [String: String] = [:] + historyItemMap["originalUrl"] = historyItem.initialURL.absoluteString + historyItemMap["title"] = historyItem.title + historyItemMap["url"] = historyItem.url.absoluteString + history.append(historyItemMap) + } + + var result: [String: Any] = [:] + result["list"] = history + result["currentIndex"] = currentIndex + + return result; + } + + @available(iOS 15.0, *) + @available(macOS 12.0, *) + @available(macCatalyst 15.0, *) + public func webView(_ webView: WKWebView, + requestMediaCapturePermissionFor origin: WKSecurityOrigin, + initiatedByFrame frame: WKFrameInfo, + type: WKMediaCaptureType, + decisionHandler: @escaping (WKPermissionDecision) -> Void) { + let origin = "\(origin.protocol)://\(origin.host)\(origin.port != 0 ? ":" + String(origin.port) : "")" + let permissionRequest = PermissionRequest(origin: origin, resources: [type.rawValue], frame: frame) + + let callback = WebViewChannelDelegate.PermissionRequestCallback() + callback.nonNullSuccess = { (response: PermissionResponse) in + if let action = response.action { + switch action { + case 1: + decisionHandler(.grant) + break + case 2: + decisionHandler(.prompt) + break + default: + decisionHandler(.deny) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: PermissionResponse?) in + decisionHandler(.deny) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onPermissionRequest(request: permissionRequest, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + @available(macOS 10.15, *) + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + preferences: WKWebpagePreferences, + decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { + self.webView(webView, decidePolicyFor: navigationAction, decisionHandler: {(navigationActionPolicy) -> Void in + decisionHandler(navigationActionPolicy, preferences) + }) + } + + @available(macOS 11.3, *) + public func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) { + if let url = response.url, let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { + let downloadStartRequest = DownloadStartRequest(url: url.absoluteString, + userAgent: nil, + contentDisposition: nil, + mimeType: response.mimeType, + contentLength: response.expectedContentLength, + suggestedFilename: suggestedFilename, + textEncodingName: response.textEncodingName) + channelDelegate?.onDownloadStartRequest(request: downloadStartRequest) + } + download.delegate = nil + // cancel the download + completionHandler(nil) + } + + @available(macOS 11.3, *) + public func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) { + let response = navigationResponse.response + if let url = response.url, let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { + let downloadStartRequest = DownloadStartRequest(url: url.absoluteString, + userAgent: nil, + contentDisposition: nil, + mimeType: response.mimeType, + contentLength: response.expectedContentLength, + suggestedFilename: response.suggestedFilename, + textEncodingName: response.textEncodingName) + channelDelegate?.onDownloadStartRequest(request: downloadStartRequest) + } + download.delegate = nil + } + + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + + if windowId != nil, !windowCreated { + decisionHandler(.cancel) + return + } + + let callback = WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback() + callback.nonNullSuccess = { (response: WKNavigationActionPolicy) in + decisionHandler(response) + return false + } + callback.defaultBehaviour = { (response: WKNavigationActionPolicy?) in + decisionHandler(.allow) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let useShouldOverrideUrlLoading = settings?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading, let channelDelegate = channelDelegate { + channelDelegate.shouldOverrideUrlLoading(navigationAction: navigationAction, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + public func webView(_ webView: WKWebView, + decidePolicyFor navigationResponse: WKNavigationResponse, + decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + if let response = navigationResponse.response as? HTTPURLResponse, response.statusCode >= 400 { + let request = WebResourceRequest.init(fromWKNavigationResponse: navigationResponse) + let errorResponse = WebResourceResponse.init(fromWKNavigationResponse: navigationResponse) + channelDelegate?.onReceivedHttpError(request: request, errorResponse: errorResponse) + } + + let useOnNavigationResponse = settings?.useOnNavigationResponse + + if useOnNavigationResponse != nil, useOnNavigationResponse! { + let callback = WebViewChannelDelegate.NavigationResponseCallback() + callback.nonNullSuccess = { (response: WKNavigationResponsePolicy) in + decisionHandler(response) + return false + } + callback.defaultBehaviour = { (response: WKNavigationResponsePolicy?) in + decisionHandler(.allow) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onNavigationResponse(navigationResponse: navigationResponse, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { + if #available(macOS 11.3, *), !navigationResponse.canShowMIMEType { + decisionHandler(.download) + return + } else { + let mimeType = navigationResponse.response.mimeType + if let url = navigationResponse.response.url, navigationResponse.isForMainFrame { + if url.scheme != "file", mimeType != nil, !mimeType!.starts(with: "text/") { + let downloadStartRequest = DownloadStartRequest(url: url.absoluteString, + userAgent: nil, + contentDisposition: nil, + mimeType: mimeType, + contentLength: navigationResponse.response.expectedContentLength, + suggestedFilename: navigationResponse.response.suggestedFilename, + textEncodingName: navigationResponse.response.textEncodingName) + channelDelegate?.onDownloadStartRequest(request: downloadStartRequest) + if useOnNavigationResponse == nil || !useOnNavigationResponse! { + decisionHandler(.cancel) + } + return + } + } + } + } + + if useOnNavigationResponse == nil || !useOnNavigationResponse! { + decisionHandler(.allow) + } + } + + public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + currentOriginalUrl = url + lastTouchPoint = nil + + disposeWebMessageChannels() + initializeWindowIdJS() + + if #available(macOS 11.0, *) { + configuration.userContentController.resetContentWorlds(windowId: windowId) + } + + channelDelegate?.onLoadStart(url: url?.absoluteString) + + inAppBrowserDelegate?.didStartNavigation(url: url) + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + initializeWindowIdJS() + + InAppWebView.credentialsProposed = [] + evaluateJavaScript(PLATFORM_READY_JS_SOURCE, completionHandler: nil) + + channelDelegate?.onLoadStop(url: url?.absoluteString) + + inAppBrowserDelegate?.didFinishNavigation(url: url) + } + + public func webView(_ view: WKWebView, + didFailProvisionalNavigation navigation: WKNavigation!, + withError error: Error) { + webView(view, didFail: navigation, withError: error) + } + + public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + InAppWebView.credentialsProposed = [] + + var urlError: URL = url ?? URL(string: "about:blank")! + var errorCode = -1 + var errorDescription = "domain=\(error._domain), code=\(error._code), \(error.localizedDescription)" + + if let info = error as? URLError { + if let failingURL = info.failingURL { + urlError = failingURL + } + errorCode = info.code.rawValue + errorDescription = info.localizedDescription + } + else if let info = error._userInfo as? [String: Any] { + if let failingUrl = info[NSURLErrorFailingURLErrorKey] as? URL { + urlError = failingUrl + } + if let failingUrlString = info[NSURLErrorFailingURLStringErrorKey] as? String, + let failingUrl = URL(string: failingUrlString) { + urlError = failingUrl + } + } + + let webResourceRequest = WebResourceRequest(url: urlError, headers: nil) + let webResourceError = WebResourceError(type: errorCode, errorDescription: errorDescription) + + channelDelegate?.onReceivedError(request: webResourceRequest, error: webResourceError) + + inAppBrowserDelegate?.didFailNavigation(url: url, error: error) + } + + public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + + if windowId != nil, !windowCreated { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNegotiate || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM { + let host = challenge.protectionSpace.host + let prot = challenge.protectionSpace.protocol + let realm = challenge.protectionSpace.realm + let port = challenge.protectionSpace.port + + let callback = WebViewChannelDelegate.ReceivedHttpAuthRequestCallback() + callback.nonNullSuccess = { (response: HttpAuthResponse) in + if let action = response.action { + switch action { + case 0: + InAppWebView.credentialsProposed = [] + // used .performDefaultHandling to mantain consistency with Android + // because .cancelAuthenticationChallenge will call webView(_:didFail:withError:) + completionHandler(.performDefaultHandling, nil) + //completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + let username = response.username + let password = response.password + let permanentPersistence = response.permanentPersistence + let persistence = (permanentPersistence) ? URLCredential.Persistence.permanent : URLCredential.Persistence.forSession + let credential = URLCredential(user: username, password: password, persistence: persistence) + completionHandler(.useCredential, credential) + break + case 2: + if InAppWebView.credentialsProposed.count == 0, let credentialStore = CredentialDatabase.credentialStore { + for (protectionSpace, credentials) in credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == prot && protectionSpace.port == port { + for credential in credentials { + InAppWebView.credentialsProposed.append(credential.value) + } + break + } + } + } + if InAppWebView.credentialsProposed.count == 0, let credential = challenge.proposedCredential { + InAppWebView.credentialsProposed.append(credential) + } + + if let credential = InAppWebView.credentialsProposed.popLast() { + completionHandler(.useCredential, credential) + } + else { + completionHandler(.performDefaultHandling, nil) + } + break + default: + InAppWebView.credentialsProposed = [] + completionHandler(.performDefaultHandling, nil) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: HttpAuthResponse?) in + completionHandler(.performDefaultHandling, nil) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + guard let serverTrust = challenge.protectionSpace.serverTrust else { + completionHandler(.performDefaultHandling, nil) + return + } + + if let scheme = challenge.protectionSpace.protocol, scheme == "https", + let sslCertificate = challenge.protectionSpace.sslCertificate { + InAppWebView.sslCertificatesMap[challenge.protectionSpace.host] = sslCertificate + } + + let callback = WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback() + callback.nonNullSuccess = { (response: ServerTrustAuthResponse) in + if let action = response.action { + switch action { + case 0: + InAppWebView.credentialsProposed = [] + completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + let exceptions = SecTrustCopyExceptions(serverTrust) + SecTrustSetExceptions(serverTrust, exceptions) + let credential = URLCredential(trust: serverTrust) + completionHandler(.useCredential, credential) + break + default: + InAppWebView.credentialsProposed = [] + completionHandler(.performDefaultHandling, nil) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: ServerTrustAuthResponse?) in + completionHandler(.performDefaultHandling, nil) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate { + let callback = WebViewChannelDelegate.ReceivedClientCertRequestCallback() + callback.nonNullSuccess = { (response: ClientCertResponse) in + if let action = response.action { + switch action { + case 0: + completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + let certificatePath = response.certificatePath + let certificatePassword = response.certificatePassword ?? ""; + + var path: String = certificatePath + do { + path = try Util.getAbsPathAsset(assetFilePath: certificatePath) + } catch {} + + if let PKCS12Data = NSData(contentsOfFile: path), + let identityAndTrust: IdentityAndTrust = self.extractIdentity(PKCS12Data: PKCS12Data, password: certificatePassword) { + let urlCredential: URLCredential = URLCredential( + identity: identityAndTrust.identityRef, + certificates: identityAndTrust.certArray as? [AnyObject], + persistence: URLCredential.Persistence.forSession); + completionHandler(.useCredential, urlCredential) + } else { + completionHandler(.performDefaultHandling, nil) + } + + break + case 2: + completionHandler(.cancelAuthenticationChallenge, nil) + break + default: + completionHandler(.performDefaultHandling, nil) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: ClientCertResponse?) in + completionHandler(.performDefaultHandling, nil) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onReceivedClientCertRequest(challenge: ClientCertChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + else { + completionHandler(.performDefaultHandling, nil) + } + } + + struct IdentityAndTrust { + + var identityRef:SecIdentity + var trust:SecTrust + var certArray:AnyObject + } + + func extractIdentity(PKCS12Data:NSData, password: String) -> IdentityAndTrust? { + var identityAndTrust:IdentityAndTrust? + var securityError:OSStatus = errSecSuccess + + var importResult: CFArray? = nil + securityError = SecPKCS12Import( + PKCS12Data as NSData, + [kSecImportExportPassphrase as String: password] as NSDictionary, + &importResult + ) + + if securityError == errSecSuccess { + let certItems:CFArray = importResult! as CFArray; + let certItemsArray:Array = certItems as Array + let dict:AnyObject? = certItemsArray.first; + if let certEntry:Dictionary = dict as? Dictionary { + // grab the identity + let identityPointer:AnyObject? = certEntry["identity"]; + let secIdentityRef:SecIdentity = (identityPointer as! SecIdentity?)!; + // grab the trust + let trustPointer:AnyObject? = certEntry["trust"]; + let trustRef:SecTrust = trustPointer as! SecTrust; + // grab the cert + let chainPointer:AnyObject? = certEntry["chain"]; + identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: chainPointer!); + } + } else { + print("Security Error: " + securityError.description) + if #available(iOS 11.3, *) { + print(SecCopyErrorMessageString(securityError,nil) ?? "") + } + } + return identityAndTrust; + } + + func createAlertDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, completionHandler: @escaping () -> Void) { + let title = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message + let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") + + let alert = NSAlert() + alert.messageText = title ?? "" + alert.alertStyle = .informational + alert.addButton(withTitle: okButton ?? "") + alert.runModal() + completionHandler() + } + + public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, + initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { + + if (isPausedTimers) { + isPausedTimersCompletionHandler = completionHandler + return + } + + let callback = WebViewChannelDelegate.JsAlertCallback() + callback.nonNullSuccess = { (response: JsAlertResponse) in + if response.handledByClient { + let action = response.action ?? 1 + switch action { + case 0: + completionHandler() + break + default: + completionHandler() + } + return false + } + return true + } + callback.defaultBehaviour = { (response: JsAlertResponse?) in + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + self.createAlertDialog(message: message, responseMessage: responseMessage, + confirmButtonTitle: confirmButtonTitle, completionHandler: completionHandler) + } + callback.error = { (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + completionHandler() + } + + if let channelDelegate = channelDelegate { + channelDelegate.onJsAlert(url: frame.request.url, message: message, isMainFrame: frame.isMainFrame, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + func createConfirmDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, cancelButtonTitle: String?, completionHandler: @escaping (Bool) -> Void) { + let dialogMessage = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message + let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") + let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "") + + let alert = NSAlert() + alert.messageText = title ?? "" + alert.alertStyle = .informational + alert.addButton(withTitle: okButton ?? "") + alert.addButton(withTitle: cancelButton ?? "") + let res = alert.runModal() + completionHandler(res == .alertFirstButtonReturn) + } + + public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (Bool) -> Void) { + let callback = WebViewChannelDelegate.JsConfirmCallback() + callback.nonNullSuccess = { (response: JsConfirmResponse) in + if response.handledByClient { + let action = response.action ?? 1 + switch action { + case 0: + completionHandler(true) + break + case 1: + completionHandler(false) + break + default: + completionHandler(false) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: JsConfirmResponse?) in + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + self.createConfirmDialog(message: message, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, completionHandler: completionHandler) + } + callback.error = { (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + completionHandler(false) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onJsConfirm(url: frame.request.url, message: message, isMainFrame: frame.isMainFrame, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + func createPromptDialog(message: String, defaultValue: String?, responseMessage: String?, confirmButtonTitle: String?, cancelButtonTitle: String?, value: String?, completionHandler: @escaping (String?) -> Void) { + let dialogMessage = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message + let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") + let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "") + + let alert = NSAlert() + alert.messageText = title ?? "" + alert.alertStyle = .informational + alert.addButton(withTitle: okButton ?? "") + alert.addButton(withTitle: cancelButton ?? "") + let txt = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) + txt.stringValue = defaultValue ?? "" + alert.accessoryView = txt + let res = alert.runModal() + + completionHandler(value != nil ? value : (res == .alertFirstButtonReturn ? txt.stringValue : nil)) + } + + public func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt message: String, defaultText defaultValue: String?, initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (String?) -> Void) { + let callback = WebViewChannelDelegate.JsPromptCallback() + callback.nonNullSuccess = { (response: JsPromptResponse) in + if response.handledByClient { + let action = response.action ?? 1 + switch action { + case 0: + completionHandler(response.value) + break + case 1: + completionHandler(nil) + break + default: + completionHandler(nil) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: JsPromptResponse?) in + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + let value = response?.value + self.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, + cancelButtonTitle: cancelButtonTitle, value: value, completionHandler: completionHandler) + } + callback.error = { (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + completionHandler(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onJsPrompt(url: frame.request.url, message: message, defaultValue: defaultValue, isMainFrame: frame.isMainFrame, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + public func webView(_ webView: WKWebView, + createWebViewWith configuration: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, + windowFeatures: WKWindowFeatures) -> WKWebView? { + InAppWebView.windowAutoincrementId += 1 + let windowId = InAppWebView.windowAutoincrementId + + let windowWebView = InAppWebView(id: nil, registrar: nil, frame: CGRect.zero, configuration: configuration, contextMenu: nil) + windowWebView.windowId = windowId + + let webViewTransport = WebViewTransport( + webView: windowWebView, + request: navigationAction.request + ) + + InAppWebView.windowWebViews[windowId] = webViewTransport + windowWebView.stopLoading() + + let createWindowAction = CreateWindowAction(navigationAction: navigationAction, windowId: windowId, windowFeatures: windowFeatures, isDialog: nil) + + let callback = WebViewChannelDelegate.CreateWindowCallback() + callback.nonNullSuccess = { (handledByClient: Bool) in + return !handledByClient + } + callback.defaultBehaviour = { (handledByClient: Bool?) in + if InAppWebView.windowWebViews[windowId] != nil { + InAppWebView.windowWebViews.removeValue(forKey: windowId) + } + self.loadUrl(urlRequest: navigationAction.request, allowingReadAccessTo: nil) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onCreateWindow(createWindowAction: createWindowAction, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + + return windowWebView + } + + public func webView(_ webView: WKWebView, + authenticationChallenge challenge: URLAuthenticationChallenge, + shouldAllowDeprecatedTLS decisionHandler: @escaping (Bool) -> Void) { + if windowId != nil, !windowCreated { + decisionHandler(false) + return + } + + let callback = WebViewChannelDelegate.ShouldAllowDeprecatedTLSCallback() + callback.nonNullSuccess = { (action: Bool) in + decisionHandler(action) + return false + } + callback.defaultBehaviour = { (action: Bool?) in + decisionHandler(false) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.shouldAllowDeprecatedTLS(challenge: challenge, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + public func webViewDidClose(_ webView: WKWebView) { + channelDelegate?.onCloseWindow() + } + + public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + channelDelegate?.onWebContentProcessDidTerminate() + } + + public func webView(_ webView: WKWebView, + didCommit navigation: WKNavigation!) { + channelDelegate?.onPageCommitVisible(url: url?.absoluteString) + } + + public func webView(_ webView: WKWebView, + didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) { + channelDelegate?.onDidReceiveServerRedirectForProvisionalNavigation() + } + +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, +// completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { +// print("contextMenuConfigurationForElement") +// let actionProvider: UIContextMenuActionProvider = { _ in +// let editMenu = UIMenu(title: "Edit...", children: [ +// UIAction(title: "Copy") { action in +// +// }, +// UIAction(title: "Duplicate") { action in +// +// } +// ]) +// return UIMenu(title: "Title", children: [ +// UIAction(title: "Share") { action in +// +// }, +// editMenu +// ]) +// } +// let contextMenuConfiguration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: actionProvider) +// //completionHandler(contextMenuConfiguration) +// completionHandler(nil) +//// onContextMenuConfigurationForElement(linkURL: elementInfo.linkURL?.absoluteString, result: nil/*{(result) -> Void in +//// if result is FlutterError { +//// print((result as! FlutterError).message ?? "") +//// } +//// else if (result as? NSObject) == FlutterMethodNotImplemented { +//// completionHandler(nil) +//// } +//// else { +//// var response: [String: Any] +//// if let r = result { +//// response = r as! [String: Any] +//// var action = response["action"] as? Int +//// action = action != nil ? action : 0; +//// switch action { +//// case 0: +//// break +//// case 1: +//// break +//// default: +//// completionHandler(nil) +//// } +//// return; +//// } +//// completionHandler(nil) +//// } +//// }*/) +// } +//// +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuDidEndForElement elementInfo: WKContextMenuElementInfo) { +// print("contextMenuDidEndForElement") +// print(elementInfo) +// //onContextMenuDidEndForElement(linkURL: elementInfo.linkURL?.absoluteString) +// } +// +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuForElement elementInfo: WKContextMenuElementInfo, +// willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) { +// print("willCommitWithAnimator") +// print(elementInfo) +//// onWillCommitWithAnimator(linkURL: elementInfo.linkURL?.absoluteString, result: nil/*{(result) -> Void in +//// if result is FlutterError { +//// print((result as! FlutterError).message ?? "") +//// } +//// else if (result as? NSObject) == FlutterMethodNotImplemented { +//// +//// } +//// else { +//// var response: [String: Any] +//// if let r = result { +//// response = r as! [String: Any] +//// var action = response["action"] as? Int +//// action = action != nil ? action : 0; +////// switch action { +////// case 0: +////// break +////// case 1: +////// break +////// default: +////// +////// } +//// return; +//// } +//// +//// } +//// }*/) +// } +// +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuWillPresentForElement elementInfo: WKContextMenuElementInfo) { +// print("contextMenuWillPresentForElement") +// print(elementInfo.linkURL) +// //onContextMenuWillPresentForElement(linkURL: elementInfo.linkURL?.absoluteString) +// } + + + // https://stackoverflow.com/a/42840541/4637638 + public func isVideoPlayerWindow(_ notificationObject: AnyObject?) -> Bool { + let nonVideoClasses = ["_UIAlertControllerShimPresenterWindow", + "UITextEffectsWindow", + "UIRemoteKeyboardWindow", + "PGHostedWindow"] + + var isVideo = true + if let obj = notificationObject { + for nonVideoClass in nonVideoClasses { + if let clazz = NSClassFromString(nonVideoClass) { + isVideo = isVideo && !(obj.isKind(of: clazz)) + } + } + } + return isVideo + } + + @objc func onEnterFullscreen(_ notification: Notification) { + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// channelDelegate?.onEnterFullscreen() +// inFullscreen = true +// } +// else + if (isVideoPlayerWindow(notification.object as AnyObject?)) { + channelDelegate?.onEnterFullscreen() + inFullscreen = true + } + } + + @objc func onExitFullscreen(_ notification: Notification) { + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// channelDelegate?.onExitFullscreen() +// inFullscreen = false +// } +// else + if (isVideoPlayerWindow(notification.object as AnyObject?)) { + channelDelegate?.onExitFullscreen() + inFullscreen = false + } + } + +// public func onContextMenuConfigurationForElement(linkURL: String?, result: FlutterResult?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onContextMenuConfigurationForElement", arguments: arguments, result: result) +// } +// +// public func onContextMenuDidEndForElement(linkURL: String?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onContextMenuDidEndForElement", arguments: arguments) +// } +// +// public func onWillCommitWithAnimator(linkURL: String?, result: FlutterResult?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onWillCommitWithAnimator", arguments: arguments, result: result) +// } +// +// public func onContextMenuWillPresentForElement(linkURL: String?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onContextMenuWillPresentForElement", arguments: arguments) +// } + + public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + if message.name.starts(with: "console") { + var messageLevel = 1 + switch (message.name) { + case "consoleLog": + messageLevel = 1 + break; + case "consoleDebug": + // on Android, console.debug is TIP + messageLevel = 0 + break; + case "consoleError": + messageLevel = 3 + break; + case "consoleInfo": + // on Android, console.info is LOG + messageLevel = 1 + break; + case "consoleWarn": + messageLevel = 2 + break; + default: + messageLevel = 1 + break; + } + let body = message.body as! [String: Any?] + let consoleMessage = body["message"] as! String + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.channelDelegate?.onConsoleMessage(message: consoleMessage, messageLevel: messageLevel) + } else if message.name == "callHandler" { + let body = message.body as! [String: Any?] + let handlerName = body["handlerName"] as! String + + if handlerName == "onPrintRequest" { + let settings = PrintJobSettings() + settings.handledByClient = true + if let printJobId = printCurrentPage(settings: settings) { + let callback = WebViewChannelDelegate.PrintRequestCallback() + callback.nonNullSuccess = { (handledByClient: Bool) in + return !handledByClient + } + callback.defaultBehaviour = { (handledByClient: Bool?) in + if let printJob = PrintJobManager.jobs[printJobId] { + printJob?.disposeNoDismiss() + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + channelDelegate?.onPrintRequest(url: url, printJobId: printJobId, callback: callback) + } + return + } + + let _callHandlerID = body["_callHandlerID"] as! Int64 + let args = body["args"] as! String + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + } + + let callback = WebViewChannelDelegate.CallJsHandlerCallback() + callback.defaultBehaviour = { (response: Any?) in + var json = "null" + if let r = response as? String { + json = r + } + + self.evaluateJavaScript(""" +if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { + window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)](\(json)); + delete window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)]; +} +""", completionHandler: nil) + } + callback.error = { (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + } + + if let channelDelegate = webView.channelDelegate { + channelDelegate.onCallJsHandler(handlerName: handlerName, args: args, callback: callback) + } + } else if message.name == "onFindResultReceived" { + let body = message.body as! [String: Any?] + let findResult = body["findResult"] as! [String: Any] + let activeMatchOrdinal = findResult["activeMatchOrdinal"] as! Int + let numberOfMatches = findResult["numberOfMatches"] as! Int + let isDoneCounting = findResult["isDoneCounting"] as! Bool + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) + } else if message.name == "onScrollChanged" { + let body = message.body as! [String: Any?] + let x = body["x"] as! Int + let y = body["y"] as! Int + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.channelDelegate?.onScrollChanged(x: x, y: y) + } else if message.name == "onCallAsyncJavaScriptResultBelowIOS14Received" { + let body = message.body as! [String: Any?] + let resultUuid = body["resultUuid"] as! String + if let result = callAsyncJavaScriptBelowIOS14Results[resultUuid] { + result([ + "value": body["value"], + "error": body["error"] + ]) + callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid) + } + } else if message.name == "onWebMessagePortMessageReceived" { + let body = message.body as! [String: Any?] + let webMessageChannelId = body["webMessageChannelId"] as! String + let index = body["index"] as! Int64 + let webMessage = body["message"] as? String + if let webMessageChannel = webMessageChannels[webMessageChannelId] { + webMessageChannel.channelDelegate?.onMessage(index: index, message: webMessage) + } + } else if message.name == "onWebMessageListenerPostMessageReceived" { + let body = message.body as! [String: Any?] + let jsObjectName = body["jsObjectName"] as! String + let messageData = body["message"] as? String + if let webMessageListener = webMessageListeners.first(where: ({($0.jsObjectName == jsObjectName)})) { + let isMainFrame = message.frameInfo.isMainFrame + + var scheme: String? = nil + var host: String? = nil + var port: Int? = nil + if #available(iOS 9.0, *) { + let sourceOrigin = message.frameInfo.securityOrigin + scheme = sourceOrigin.protocol + host = sourceOrigin.host + port = sourceOrigin.port + } else if let url = message.frameInfo.request.url { + scheme = url.scheme + host = url.host + port = url.port + } + + if !webMessageListener.isOriginAllowed(scheme: scheme, host: host, port: port) { + return + } + + var sourceOrigin: URL? = nil + if let scheme = scheme, !scheme.isEmpty, let host = host, !host.isEmpty { + sourceOrigin = URL(string: "\(scheme)://\(host)\(port != nil && port != 0 ? ":" + String(port!) : "")") + } + webMessageListener.channelDelegate?.onPostMessage(message: messageData, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame) + } + } + } + + public func scrollTo(x: Int, y: Int, animated: Bool) { + evaluateJavaScript("window.scrollTo({left: \(x), top: \(y), behavior: \(animated ? "'smooth'" : "'auto'")})") + } + + public func scrollBy(x: Int, y: Int, animated: Bool) { + evaluateJavaScript("window.scrollBy({left: \(x), top: \(y), behavior: \(animated ? "'smooth'" : "'auto'")})") + } + + + public func pauseTimers() { + if !isPausedTimers { + isPausedTimers = true + let script = "alert();"; + self.evaluateJavaScript(script, completionHandler: nil) + } + } + + public func resumeTimers() { + if isPausedTimers { + if let completionHandler = isPausedTimersCompletionHandler { + self.isPausedTimersCompletionHandler = nil + completionHandler() + } + isPausedTimers = false + } + } + + public func printCurrentPage(settings: PrintJobSettings? = nil, + completionHandler: PrintJobController.CompletionHandler? = nil) -> String? { + if #available(macOS 11.0, *) { + var printJobId: String? = nil + if let settings = settings, settings.handledByClient { + printJobId = NSUUID().uuidString + } + + let printInfo = NSPrintInfo() + + if let settings = settings { + if let orientationValue = settings.orientation, + let orientation = NSPrintInfo.PaperOrientation.init(rawValue: orientationValue) { + printInfo.orientation = orientation + } + if let margins = settings.margins { + printInfo.topMargin = margins.top + printInfo.rightMargin = margins.right + printInfo.bottomMargin = margins.bottom + printInfo.leftMargin = margins.left + } + } + let printOperation = printOperation(with: printInfo) + printOperation.jobTitle = settings?.jobName ?? (title ?? url?.absoluteString ?? "") + " Document" + printOperation.view?.frame = bounds + printOperation.printPanel.options.insert(.showsOrientation) + printOperation.printPanel.options.insert(.showsPaperSize) + printOperation.printPanel.options.insert(.showsScaling) + + if let id = printJobId { + let printJob = PrintJobController(id: id, job: printOperation, settings: settings) + PrintJobManager.jobs[id] = printJob + printJob.present(parentWindow: window, completionHandler: completionHandler) + } else if let window = window { + printJobCompletionHandler = completionHandler + printOperation.runModal(for: window, delegate: self, didRun: #selector(printOperationDidRun), contextInfo: nil) + } else { + printView(self) + } + + return printJobId + } else { + printView(self) + } + return nil + } + + @objc func printOperationDidRun(printOperation: NSPrintOperation, + success: Bool, + contextInfo: UnsafeMutableRawPointer?) { + if let completionHandler = printJobCompletionHandler { + completionHandler(printOperation, success, contextInfo) + printJobCompletionHandler = nil + } + } + + public func getContentHeight(completionHandler: @escaping ((Int64?, Error?) -> Void)) { + evaluateJavaScript("document.body.scrollHeight") { scrollHeight, error in + if let error = error { + completionHandler(nil, error) + } else { + completionHandler(Int64(scrollHeight as? Double ?? 0.0), nil) + } + } + } + + public func getOriginalUrl() -> URL? { + return currentOriginalUrl + } + + public func getSelectedText(completionHandler: @escaping (Any?, Error?) -> Void) { + if configuration.preferences.javaScriptEnabled { + evaluateJavaScript(PluginScriptsUtil.GET_SELECTED_TEXT_JS_SOURCE, completionHandler: completionHandler) + } else { + completionHandler(nil, nil) + } + } + + public func getHitTestResult(completionHandler: @escaping (HitTestResult) -> Void) { + if configuration.preferences.javaScriptEnabled, let lastTouchLocation = lastTouchPoint { + self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(lastTouchLocation.x),\(lastTouchLocation.y))", completionHandler: {(value, error) in + if error != nil { + print("getHitTestResult error: \(error?.localizedDescription ?? "")") + completionHandler(HitTestResult(type: .unknownType, extra: nil)) + } else if let value = value as? [String: Any?] { + let hitTestResult = HitTestResult.fromMap(map: value)! + completionHandler(hitTestResult) + } else { + completionHandler(HitTestResult(type: .unknownType, extra: nil)) + } + }) + } else { + completionHandler(HitTestResult(type: .unknownType, extra: nil)) + } + } + + public func requestFocusNodeHref(completionHandler: @escaping ([String: Any?]?, Error?) -> Void) { + if configuration.preferences.javaScriptEnabled { + // add some delay to make it sure _lastAnchorOrImageTouched is updated + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched", completionHandler: {(value, error) in + let lastAnchorOrImageTouched = value as? [String: Any?] + completionHandler(lastAnchorOrImageTouched, error) + }) + } + } else { + completionHandler(nil, nil) + } + } + + public func requestImageRef(completionHandler: @escaping ([String: Any?]?, Error?) -> Void) { + if configuration.preferences.javaScriptEnabled { + // add some delay to make it sure _lastImageTouched is updated + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched", completionHandler: {(value, error) in + let lastImageTouched = value as? [String: Any?] + completionHandler(lastImageTouched, error) + }) + } + } else { + completionHandler(nil, nil) + } + } + + public func clearFocus() { + self.enclosingScrollView?.subviews.first?.resignFirstResponder() + } + + public func getCertificate() -> SslCertificate? { + guard let scheme = url?.scheme, + scheme == "https", + let host = url?.host, + let sslCertificate = InAppWebView.sslCertificatesMap[host] else { + return nil + } + return sslCertificate + } + + public func isSecureContext(completionHandler: @escaping (_ isSecureContext: Bool) -> Void) { + evaluateJavascript(source: "window.isSecureContext") { (isSecureContext) in + if let isSecureContext = isSecureContext { + completionHandler(isSecureContext as? Bool ?? false) + return + } + completionHandler(false) + } + } + + public func canScrollVertically() -> Bool { + return enclosingScrollView?.contentSize.height ?? 0 > self.frame.height + } + + public func canScrollHorizontally() -> Bool { + return enclosingScrollView?.contentSize.width ?? 0 > self.frame.width + } + + public func createWebMessageChannel(completionHandler: ((WebMessageChannel) -> Void)? = nil) -> WebMessageChannel { + let id = NSUUID().uuidString + let webMessageChannel = WebMessageChannel(id: id) + webMessageChannel.initJsInstance(webView: self, completionHandler: completionHandler) + webMessageChannels[id] = webMessageChannel + + return webMessageChannel + } + + public func postWebMessage(message: WebMessage, targetOrigin: String, completionHandler: ((Any?) -> Void)? = nil) throws { + var portsString = "null" + if let ports = message.ports { + var portArrayString: [String] = [] + for port in ports { + if port.isStarted { + throw NSError(domain: "Port is already started", code: 0) + } + if port.isClosed || port.isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + port.isTransferred = true + portArrayString.append("\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)['\(port.webMessageChannel!.id)'].\(port.name)") + } + portsString = "[" + portArrayString.joined(separator: ", ") + "]" + } + let data = message.data?.replacingOccurrences(of: "\'", with: "\\'") ?? "null" + let url = URL(string: targetOrigin)?.absoluteString ?? "*" + let source = """ + (function() { + window.postMessage('\(data)', '\(url)', \(portsString)); + })(); + """ + evaluateJavascript(source: source, completionHandler: completionHandler) + message.dispose() + } + + public func addWebMessageListener(webMessageListener: WebMessageListener) throws { + if webMessageListeners.map({ ($0.jsObjectName) }).contains(webMessageListener.jsObjectName) { + throw NSError(domain: "jsObjectName \(webMessageListener.jsObjectName) was already added.", code: 0) + } + try webMessageListener.assertOriginRulesValid() + webMessageListener.initJsInstance(webView: self) + webMessageListeners.append(webMessageListener) + } + + public func disposeWebMessageChannels() { + for webMessageChannel in webMessageChannels.values { + webMessageChannel.dispose() + } + webMessageChannels.removeAll() + } + + public func getScrollX(completionHandler: @escaping ((Int64?, Error?) -> Void)) { + evaluateJavaScript("window.scrollX") { scrollX, error in + if let error = error { + completionHandler(nil, error) + } else { + completionHandler(Int64(scrollX as? Double ?? 0.0), nil) + } + } + } + + public func getScrollY(completionHandler: @escaping ((Int64?, Error?) -> Void)) { + evaluateJavaScript("window.scrollY") { scrollY, error in + if let error = error { + completionHandler(nil, error) + } else { + completionHandler(Int64(scrollY as? Double ?? 0.0), nil) + } + } + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + printJobCompletionHandler = nil + removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.url)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.title)) + if #available(macOS 12.0, *) { + removeObserver(self, forKeyPath: #keyPath(WKWebView.cameraCaptureState)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.microphoneCaptureState)) + } + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// removeObserver(self, forKeyPath: #keyPath(WKWebView.fullscreenState)) +// } + resumeTimers() + stopLoading() + disposeWebMessageChannels() + for webMessageListener in webMessageListeners { + webMessageListener.dispose() + } + webMessageListeners.removeAll() + if windowId == nil { + configuration.userContentController.removeAllPluginScriptMessageHandlers() + configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessagePortMessageReceived") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessageListenerPostMessageReceived") + configuration.userContentController.removeAllUserScripts() + if #available(macOS 10.13, *) { + configuration.userContentController.removeAllContentRuleLists() + } + } else if let wId = windowId, InAppWebView.windowWebViews[wId] != nil { + InAppWebView.windowWebViews.removeValue(forKey: wId) + } + configuration.userContentController.dispose(windowId: windowId) + NotificationCenter.default.removeObserver(self) + for imp in customIMPs { + imp_removeBlock(imp) + } + uiDelegate = nil + navigationDelegate = nil + isPausedTimersCompletionHandler = nil + SharedLastTouchPointTimestamp.removeValue(forKey: self) + callAsyncJavaScriptBelowIOS14Results.removeAll() + super.removeFromSuperview() + } + + deinit { + debugPrint("InAppWebView - dealloc") + } +} diff --git a/macos/Classes/InAppWebView/InAppWebViewSettings.swift b/macos/Classes/InAppWebView/InAppWebViewSettings.swift new file mode 100755 index 00000000..1d9f7f96 --- /dev/null +++ b/macos/Classes/InAppWebView/InAppWebViewSettings.swift @@ -0,0 +1,136 @@ +// +// InAppWebViewSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo on 21/10/18. +// + +import Foundation +import WebKit + +@objcMembers +public class InAppWebViewSettings: ISettings { + + var useShouldOverrideUrlLoading = false + var useOnLoadResource = false + var useOnDownloadStart = false + var clearCache = false + var userAgent = "" + var applicationNameForUserAgent = "" + var javaScriptEnabled = true + var javaScriptCanOpenWindowsAutomatically = false + var mediaPlaybackRequiresUserGesture = true + var verticalScrollBarEnabled = true + var horizontalScrollBarEnabled = true + var resourceCustomSchemes: [String] = [] + var contentBlockers: [[String: [String : Any]]] = [] + var minimumFontSize = 0 + var useShouldInterceptAjaxRequest = false + var useShouldInterceptFetchRequest = false + var incognito = false + var cacheEnabled = true + var transparentBackground = false + var disableVerticalScroll = false + var disableHorizontalScroll = false + var disableContextMenu = false + var supportZoom = true + var allowUniversalAccessFromFileURLs = false + var allowFileAccessFromFileURLs = false + + var disallowOverScroll = false + var enableViewportScale = false + var suppressesIncrementalRendering = false + var allowsAirPlayForMediaPlayback = true + var allowsBackForwardNavigationGestures = true + var allowsLinkPreview = true + var ignoresViewportScaleLimits = false + var allowsInlineMediaPlayback = false + var allowsPictureInPictureMediaPlayback = true + var isFraudulentWebsiteWarningEnabled = true + var selectionGranularity = 0 + var dataDetectorTypes: [String] = ["NONE"] // WKDataDetectorTypeNone + var preferredContentMode = 0 + var sharedCookiesEnabled = false + var automaticallyAdjustsScrollIndicatorInsets = false + var accessibilityIgnoresInvertColors = false + var decelerationRate = "NORMAL" // UIScrollView.DecelerationRate.normal + var alwaysBounceVertical = false + var alwaysBounceHorizontal = false + var scrollsToTop = true + var isPagingEnabled = false + var maximumZoomScale = 1.0 + var minimumZoomScale = 1.0 + var contentInsetAdjustmentBehavior = 2 // UIScrollView.ContentInsetAdjustmentBehavior.never + var isDirectionalLockEnabled = false + var mediaType: String? = nil + var pageZoom = 1.0 + var limitsNavigationsToAppBoundDomains = false + var useOnNavigationResponse = false + var applePayAPIEnabled = false + var allowingReadAccessTo: String? = nil + var disableLongPressContextMenuOnLinks = false + var disableInputAccessoryView = false + var underPageBackgroundColor: String? + var isTextInteractionEnabled = true + var isSiteSpecificQuirksModeEnabled = true + var upgradeKnownHostsToHTTPS = true + var isElementFullscreenEnabled = true + var isFindInteractionEnabled = false + var minimumViewportInset: NSEdgeInsets? = nil + var maximumViewportInset: NSEdgeInsets? = nil + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> InAppWebViewSettings { + let _ = super.parse(settings: settings) + if #available(iOS 13.0, *) {} else { + applePayAPIEnabled = false + } + return self + } + + override func getRealSettings(obj: InAppWebView?) -> [String: Any?] { + var realSettings: [String: Any?] = toMap() + if let webView = obj { + let configuration = webView.configuration + if #available(iOS 9.0, *) { + realSettings["userAgent"] = webView.customUserAgent + realSettings["applicationNameForUserAgent"] = configuration.applicationNameForUserAgent + realSettings["allowsAirPlayForMediaPlayback"] = configuration.allowsAirPlayForMediaPlayback + realSettings["allowsLinkPreview"] = webView.allowsLinkPreview + } + realSettings["javaScriptCanOpenWindowsAutomatically"] = configuration.preferences.javaScriptCanOpenWindowsAutomatically + if #available(macOS 10.12, *) { + realSettings["mediaPlaybackRequiresUserGesture"] = configuration.mediaTypesRequiringUserActionForPlayback == .all + } + realSettings["minimumFontSize"] = configuration.preferences.minimumFontSize + realSettings["suppressesIncrementalRendering"] = configuration.suppressesIncrementalRendering + realSettings["allowsBackForwardNavigationGestures"] = webView.allowsBackForwardNavigationGestures + if #available(macOS 10.15, *) { + realSettings["isFraudulentWebsiteWarningEnabled"] = configuration.preferences.isFraudulentWebsiteWarningEnabled + realSettings["preferredContentMode"] = configuration.defaultWebpagePreferences.preferredContentMode.rawValue + } + realSettings["allowUniversalAccessFromFileURLs"] = configuration.value(forKey: "allowUniversalAccessFromFileURLs") + realSettings["allowFileAccessFromFileURLs"] = configuration.preferences.value(forKey: "allowFileAccessFromFileURLs") + realSettings["javaScriptEnabled"] = configuration.preferences.javaScriptEnabled + if #available(macOS 11.0, *) { + realSettings["mediaType"] = webView.mediaType + realSettings["pageZoom"] = Float(webView.pageZoom) + realSettings["limitsNavigationsToAppBoundDomains"] = configuration.limitsNavigationsToAppBoundDomains + realSettings["javaScriptEnabled"] = configuration.defaultWebpagePreferences.allowsContentJavaScript + } + if #available(macOS 11.3, *) { + realSettings["isTextInteractionEnabled"] = configuration.preferences.isTextInteractionEnabled + realSettings["upgradeKnownHostsToHTTPS"] = configuration.upgradeKnownHostsToHTTPS + } + if #available(macOS 12.0, *) { + realSettings["underPageBackgroundColor"] = webView.underPageBackgroundColor.hexString + realSettings["isSiteSpecificQuirksModeEnabled"] = configuration.preferences.isSiteSpecificQuirksModeEnabled + realSettings["isElementFullscreenEnabled"] = configuration.preferences.isElementFullscreenEnabled + } + } + return realSettings + } +} diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift new file mode 100644 index 00000000..79eb1feb --- /dev/null +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift @@ -0,0 +1,75 @@ +// +// WebMessageChannel.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation +import FlutterMacOS + +public class WebMessageChannel : FlutterMethodCallDelegate { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_channel_" + var id: String + var channelDelegate: WebMessageChannelChannelDelegate? + weak var webView: InAppWebView? + var ports: [WebMessagePort] = [] + + public init(id: String) { + self.id = id + super.init() + let channel = FlutterMethodChannel(name: WebMessageChannel.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + self.channelDelegate = WebMessageChannelChannelDelegate(webMessageChannel: self, channel: channel) + self.ports = [ + WebMessagePort(name: "port1", webMessageChannel: self), + WebMessagePort(name: "port2", webMessageChannel: self) + ] + } + + public func initJsInstance(webView: InAppWebView, completionHandler: ((WebMessageChannel) -> Void)? = nil) { + self.webView = webView + if let webView = self.webView { + webView.evaluateJavascript(source: """ + (function() { + \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"] = new MessageChannel(); + })(); + """) { (_) in + completionHandler?(self) + } + } else { + completionHandler?(self) + } + } + + public func toMap() -> [String:Any?] { + return [ + "id": id + ] + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + for port in ports { + port.dispose() + } + ports.removeAll() + webView?.evaluateJavascript(source: """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"]; + if (webMessageChannel != null) { + webMessageChannel.port1.close(); + webMessageChannel.port2.close(); + delete \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"]; + } + })(); + """) + webView = nil + } + + deinit { + debugPrint("WebMessageChannel - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift new file mode 100644 index 00000000..1e841100 --- /dev/null +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift @@ -0,0 +1,105 @@ +// +// WebMessageChannelChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import FlutterMacOS + +public class WebMessageChannelChannelDelegate : ChannelDelegate { + private weak var webMessageChannel: WebMessageChannel? + + public init(webMessageChannel: WebMessageChannel, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webMessageChannel = webMessageChannel + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "setWebMessageCallback": + if let _ = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + do { + try port.setWebMessageCallback { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + + } else { + result(true) + } + break + case "postMessage": + if let webView = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + let message = arguments!["message"] as! [String: Any?] + + var webMessagePorts: [WebMessagePort] = [] + let portsMap = message["ports"] as? [[String: Any?]] + if let portsMap = portsMap { + for portMap in portsMap { + let webMessageChannelId = portMap["webMessageChannelId"] as! String + let index = portMap["index"] as! Int + if let webMessageChannel = webView.webMessageChannels[webMessageChannelId] { + webMessagePorts.append(webMessageChannel.ports[index]) + } + } + } + let webMessage = WebMessage(data: message["data"] as? String, ports: webMessagePorts) + do { + try port.postMessage(message: webMessage) { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + } else { + result(true) + } + break + case "close": + if let _ = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + do { + try port.close { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + } else { + result(true) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onMessage(index: Int64, message: String?) { + let arguments: [String:Any?] = [ + "index": index, + "message": message + ] + channel?.invokeMethod("onMessage", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webMessageChannel = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageListener.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageListener.swift new file mode 100644 index 00000000..b5691ba9 --- /dev/null +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageListener.swift @@ -0,0 +1,184 @@ +// +// WebMessageListener.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class WebMessageListener : FlutterMethodCallDelegate { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_listener_" + var jsObjectName: String + var allowedOriginRules: Set + var channelDelegate: WebMessageListenerChannelDelegate? + weak var webView: InAppWebView? + + public init(jsObjectName: String, allowedOriginRules: Set) { + self.jsObjectName = jsObjectName + self.allowedOriginRules = allowedOriginRules + super.init() + let channel = FlutterMethodChannel(name: WebMessageListener.METHOD_CHANNEL_NAME_PREFIX + self.jsObjectName, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + self.channelDelegate = WebMessageListenerChannelDelegate(webMessageListener: self, channel: channel) + } + + public func assertOriginRulesValid() throws { + for (index, originRule) in allowedOriginRules.enumerated() { + if originRule.isEmpty { + throw NSError(domain: "allowedOriginRules[\(index)] is empty", code: 0) + } + if originRule == "*" { + continue + } + if let url = URL(string: originRule) { + guard let scheme = url.scheme else { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if scheme == "http" || scheme == "https", url.host == nil || url.host!.isEmpty { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if scheme != "http", scheme != "https", url.host != nil || url.port != nil { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if url.host == nil || url.host!.isEmpty, url.port != nil { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if !url.path.isEmpty { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if let hostname = url.host { + if let firstIndex = hostname.firstIndex(of: "*") { + let distance = hostname.distance(from: hostname.startIndex, to: firstIndex) + if distance != 0 || (distance == 0 && hostname.prefix(2) != "*.") { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + if hostname.hasPrefix("[") { + if !hostname.hasSuffix("]") { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + let fromIndex = hostname.index(hostname.startIndex, offsetBy: 1) + let toIndex = hostname.index(hostname.startIndex, offsetBy: hostname.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + let ipv6 = String(hostname[indexRange]) + if !Util.isIPv6(address: ipv6) { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + } + } else { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + } + + public func initJsInstance(webView: InAppWebView) { + self.webView = webView + if let webView = self.webView { + let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") + let allowedOriginRulesString = allowedOriginRules.map { (allowedOriginRule) -> String in + if allowedOriginRule == "*" { + return "'*'" + } + let rule = URL(string: allowedOriginRule)! + let host = rule.host != nil ? "'" + rule.host!.replacingOccurrences(of: "\'", with: "\\'") + "'" : "null" + return """ + {scheme: '\(rule.scheme!)', host: \(host), port: \(rule.port != nil ? String(rule.port!) : "null")} + """ + }.joined(separator: ", ") + let source = """ + (function() { + var allowedOriginRules = [\(allowedOriginRulesString)]; + var isPageBlank = window.location.href === "about:blank"; + var scheme = !isPageBlank ? window.location.protocol.replace(":", "") : null; + var host = !isPageBlank ? window.location.hostname : null; + var port = !isPageBlank ? window.location.port : null; + if (window.\(JAVASCRIPT_BRIDGE_NAME)._isOriginAllowed(allowedOriginRules, scheme, host, port)) { + window['\(jsObjectNameEscaped)'] = new FlutterInAppWebViewWebMessageListener('\(jsObjectNameEscaped)'); + } + })(); + """ + webView.configuration.userContentController.addPluginScript(PluginScript( + groupName: "WebMessageListener-" + jsObjectName, + source: source, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onWebMessageListenerPostMessageReceived"] + )) + webView.configuration.userContentController.sync(scriptMessageHandler: webView) + } + } + + public static func fromMap(map: [String:Any?]?) -> WebMessageListener? { + guard let map = map else { + return nil + } + return WebMessageListener( + jsObjectName: map["jsObjectName"] as! String, + allowedOriginRules: Set(map["allowedOriginRules"] as! [String]) + ) + } + + public func isOriginAllowed(scheme: String?, host: String?, port: Int?) -> Bool { + for allowedOriginRule in allowedOriginRules { + if allowedOriginRule == "*" { + return true + } + if scheme == nil || scheme!.isEmpty { + continue + } + if scheme == nil || scheme!.isEmpty, host == nil || host!.isEmpty, port == nil || port == 0 { + continue + } + if let rule = URL(string: allowedOriginRule) { + let rulePort = rule.port == nil || rule.port == 0 ? (rule.scheme == "https" ? 443 : 80) : rule.port! + let currentPort = port == nil || port == 0 ? (scheme == "https" ? 443 : 80) : port! + var IPv6: String? = nil + if let hostname = rule.host, hostname.hasPrefix("[") { + let fromIndex = hostname.index(hostname.startIndex, offsetBy: 1) + let toIndex = hostname.index(hostname.startIndex, offsetBy: hostname.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + do { + IPv6 = try Util.normalizeIPv6(address: String(hostname[indexRange])) + } catch {} + } + var hostIPv6: String? = nil + if let host = host, Util.isIPv6(address: host) { + do { + hostIPv6 = try Util.normalizeIPv6(address: host) + } catch {} + } + + let schemeAllowed = scheme != nil && !scheme!.isEmpty && scheme == rule.scheme + + let hostAllowed = rule.host == nil || + rule.host!.isEmpty || + host == rule.host || + (rule.host!.hasPrefix("*") && host != nil && host!.hasSuffix(rule.host!.split(separator: "*", omittingEmptySubsequences: false)[1])) || + (hostIPv6 != nil && IPv6 != nil && hostIPv6 == IPv6) + + let portAllowed = rulePort == currentPort + + if schemeAllowed, hostAllowed, portAllowed { + return true + } + } + } + return false + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + webView = nil + } + + deinit { + debugPrint("WebMessageListener - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift new file mode 100644 index 00000000..76b91d03 --- /dev/null +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift @@ -0,0 +1,72 @@ +// +// WebMessageListenerChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import FlutterMacOS + +public class WebMessageListenerChannelDelegate : ChannelDelegate { + private weak var webMessageListener: WebMessageListener? + + public init(webMessageListener: WebMessageListener, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webMessageListener = webMessageListener + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "postMessage": + if let webView = webMessageListener?.webView, let jsObjectName = webMessageListener?.jsObjectName { + let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") + let messageEscaped = (arguments!["message"] as! String).replacingOccurrences(of: "\'", with: "\\'") + let source = """ + (function() { + var webMessageListener = window['\(jsObjectNameEscaped)']; + if (webMessageListener != null) { + var event = {data: '\(messageEscaped)'}; + if (webMessageListener.onmessage != null) { + webMessageListener.onmessage(event); + } + for (var listener of webMessageListener.listeners) { + listener(event); + } + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + result(true) + } + } else { + result(true) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onPostMessage(message: String?, sourceOrigin: URL?, isMainFrame: Bool) { + let arguments: [String:Any?] = [ + "message": message, + "sourceOrigin": sourceOrigin?.absoluteString, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onPostMessage", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webMessageListener = nil + } + + deinit { + dispose() + } +} + diff --git a/macos/Classes/InAppWebView/WebViewChannelDelegate.swift b/macos/Classes/InAppWebView/WebViewChannelDelegate.swift new file mode 100644 index 00000000..b32b9baa --- /dev/null +++ b/macos/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -0,0 +1,1085 @@ +// +// WebViewChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class WebViewChannelDelegate : ChannelDelegate { + private weak var webView: InAppWebView? + + public init(webView: InAppWebView, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webView = webView + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + guard let method = WebViewChannelDelegateMethods.init(rawValue: call.method) else { + result(FlutterMethodNotImplemented) + return + } + + switch method { + case .getUrl: + result(webView?.url?.absoluteString) + break + case .getTitle: + result(webView?.title) + break + case .getProgress: + result( (webView != nil) ? Int(webView!.estimatedProgress * 100) : nil ) + break + case .loadUrl: + let urlRequest = arguments!["urlRequest"] as! [String:Any?] + let allowingReadAccessTo = arguments!["allowingReadAccessTo"] as? String + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = allowingReadAccessTo { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + } + webView?.loadUrl(urlRequest: URLRequest.init(fromPluginMap: urlRequest), allowingReadAccessTo: allowingReadAccessToURL) + result(true) + break + case .postUrl: + if let webView = webView { + let url = arguments!["url"] as! String + let postData = arguments!["postData"] as! FlutterStandardTypedData + webView.postUrl(url: URL(string: url)!, postData: postData.data) + } + result(true) + break + case .loadData: + let data = arguments!["data"] as! String + let mimeType = arguments!["mimeType"] as! String + let encoding = arguments!["encoding"] as! String + let baseUrl = URL(string: arguments!["baseUrl"] as! String)! + let allowingReadAccessTo = arguments!["allowingReadAccessTo"] as? String + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = allowingReadAccessTo { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + } + webView?.loadData(data: data, mimeType: mimeType, encoding: encoding, baseUrl: baseUrl, allowingReadAccessTo: allowingReadAccessToURL) + result(true) + break + case .loadFile: + let assetFilePath = arguments!["assetFilePath"] as! String + + do { + try webView?.loadFile(assetFilePath: assetFilePath) + } + catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + return + } + result(true) + break + case .evaluateJavascript: + if let webView = webView { + let source = arguments!["source"] as! String + let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] + if #available(macOS 11.0, *), let contentWorldMap = contentWorldMap { + let contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: webView.windowId)! + webView.evaluateJavascript(source: source, contentWorld: contentWorld) { (value) in + result(value) + } + } else { + webView.evaluateJavascript(source: source) { (value) in + result(value) + } + } + } + else { + result(nil) + } + break + case .injectJavascriptFileFromUrl: + let urlFile = arguments!["urlFile"] as! String + let scriptHtmlTagAttributes = arguments!["scriptHtmlTagAttributes"] as? [String:Any?] + webView?.injectJavascriptFileFromUrl(urlFile: urlFile, scriptHtmlTagAttributes: scriptHtmlTagAttributes) + result(true) + break + case .injectCSSCode: + let source = arguments!["source"] as! String + webView?.injectCSSCode(source: source) + result(true) + break + case .injectCSSFileFromUrl: + let urlFile = arguments!["urlFile"] as! String + let cssLinkHtmlTagAttributes = arguments!["cssLinkHtmlTagAttributes"] as? [String:Any?] + webView?.injectCSSFileFromUrl(urlFile: urlFile, cssLinkHtmlTagAttributes: cssLinkHtmlTagAttributes) + result(true) + break + case .reload: + webView?.reload() + result(true) + break + case .goBack: + webView?.goBack() + result(true) + break + case .canGoBack: + result(webView?.canGoBack ?? false) + break + case .goForward: + webView?.goForward() + result(true) + break + case .canGoForward: + result(webView?.canGoForward ?? false) + break + case .goBackOrForward: + let steps = arguments!["steps"] as! Int + webView?.goBackOrForward(steps: steps) + result(true) + break + case .canGoBackOrForward: + let steps = arguments!["steps"] as! Int + result(webView?.canGoBackOrForward(steps: steps) ?? false) + break + case .stopLoading: + webView?.stopLoading() + result(true) + break + case .isLoading: + result(webView?.isLoading ?? false) + break + case .takeScreenshot: + if let webView = webView, #available(macOS 10.13, *) { + let screenshotConfiguration = arguments!["screenshotConfiguration"] as? [String: Any?] + webView.takeScreenshot(with: screenshotConfiguration, completionHandler: { (screenshot) -> Void in + result(screenshot) + }) + } + else { + result(nil) + } + break + case .setSettings: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + let inAppBrowserSettings = InAppBrowserSettings() + let inAppBrowserSettingsMap = arguments!["settings"] as! [String: Any] + let _ = inAppBrowserSettings.parse(settings: inAppBrowserSettingsMap) + iabController.setSettings(newSettings: inAppBrowserSettings, newSettingsMap: inAppBrowserSettingsMap) + } else { + let inAppWebViewSettings = InAppWebViewSettings() + let inAppWebViewSettingsMap = arguments!["settings"] as! [String: Any] + let _ = inAppWebViewSettings.parse(settings: inAppWebViewSettingsMap) + webView?.setSettings(newSettings: inAppWebViewSettings, newSettingsMap: inAppWebViewSettingsMap) + } + result(true) + break + case .getSettings: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + result(iabController.getSettings()) + } else { + result(webView?.getSettings()) + } + break + case .close: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.close() + result(true) + } else { + result(FlutterMethodNotImplemented) + } + break + case .show: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.show() + result(true) + } else { + result(FlutterMethodNotImplemented) + } + break + case .hide: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.hide() + result(true) + } else { + result(FlutterMethodNotImplemented) + } + break + case .getCopyBackForwardList: + result(webView?.getCopyBackForwardList()) + break + case .clearCache: + webView?.clearCache() + result(true) + break + case .scrollTo: + let x = arguments!["x"] as! Int + let y = arguments!["y"] as! Int + let animated = arguments!["animated"] as! Bool + webView?.scrollTo(x: x, y: y, animated: animated) + result(true) + break + case .scrollBy: + let x = arguments!["x"] as! Int + let y = arguments!["y"] as! Int + let animated = arguments!["animated"] as! Bool + webView?.scrollBy(x: x, y: y, animated: animated) + result(true) + break + case .pauseTimers: + webView?.pauseTimers() + result(true) + break + case .resumeTimers: + webView?.resumeTimers() + result(true) + break + case .printCurrentPage: + if let webView = webView { + let settings = PrintJobSettings() + if let settingsMap = arguments!["settings"] as? [String: Any?] { + let _ = settings.parse(settings: settingsMap) + } + result(webView.printCurrentPage(settings: settings)) + } else { + result(nil) + } + break + case .getContentHeight: + webView?.getContentHeight { contentHeight, error in + if let error = error { + print(error) + result(nil) + return + } + result(contentHeight) + } + break + case .zoomBy: +// let zoomFactor = (arguments!["zoomFactor"] as! NSNumber).floatValue +// let animated = arguments!["animated"] as! Bool +// webView?.zoomBy(zoomFactor: zoomFactor, animated: animated) + result(true) + break + case .reloadFromOrigin: + webView?.reloadFromOrigin() + result(true) + break + case .getOriginalUrl: + result(webView?.getOriginalUrl()?.absoluteString) + break + case .getZoomScale: + result(nil) + break + case .hasOnlySecureContent: + result(webView?.hasOnlySecureContent ?? false) + break + case .getSelectedText: + if let webView = webView { + webView.getSelectedText { (value, error) in + if let err = error { + print(err.localizedDescription) + result("") + return + } + result(value) + } + } + else { + result(nil) + } + break + case .getHitTestResult: + if let webView = webView { + webView.getHitTestResult { (hitTestResult) in + result(hitTestResult.toMap()) + } + } + else { + result(nil) + } + break + case .clearFocus: + webView?.clearFocus() + result(true) + break + case .setContextMenu: + if let webView = webView { + let contextMenu = arguments!["contextMenu"] as? [String: Any] + webView.contextMenu = contextMenu + result(true) + } else { + result(false) + } + break + case .requestFocusNodeHref: + if let webView = webView { + webView.requestFocusNodeHref { (value, error) in + if let err = error { + print(err.localizedDescription) + result(nil) + return + } + result(value) + } + } else { + result(nil) + } + break + case .requestImageRef: + if let webView = webView { + webView.requestImageRef { (value, error) in + if let err = error { + print(err.localizedDescription) + result(nil) + return + } + result(value) + } + } else { + result(nil) + } + break + case .getScrollX: + if let webView = webView { + webView.getScrollX { scrollX, error in + if let error = error { + print(error) + result(nil) + return + } + result(scrollX) + } + + } else { + result(nil) + } + break + case .getScrollY: + if let webView = webView { + webView.getScrollY { scrollY, error in + if let error = error { + print(error) + result(nil) + return + } + result(scrollY) + } + + } else { + result(nil) + } + break + case .getCertificate: + result(webView?.getCertificate()?.toMap()) + break + case .addUserScript: + if let webView = webView { + let userScriptMap = arguments!["userScript"] as! [String: Any?] + let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView.windowId)! + webView.configuration.userContentController.addUserOnlyScript(userScript) + webView.configuration.userContentController.sync(scriptMessageHandler: webView) + } + result(true) + break + case .removeUserScript: + let index = arguments!["index"] as! Int + let userScriptMap = arguments!["userScript"] as! [String: Any?] + let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView?.windowId)! + webView?.configuration.userContentController.removeUserOnlyScript(at: index, injectionTime: userScript.injectionTime) + result(true) + break + case .removeUserScriptsByGroupName: + let groupName = arguments!["groupName"] as! String + webView?.configuration.userContentController.removeUserOnlyScripts(with: groupName) + result(true) + break + case .removeAllUserScripts: + webView?.configuration.userContentController.removeAllUserOnlyScripts() + result(true) + break + case .callAsyncJavaScript: + if let webView = webView { + if #available(macOS 11.0, *) { // on iOS 14.0, for some reason, it crashes + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + var contentWorld = WKContentWorld.page + if let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] { + contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: webView.windowId)! + } + webView.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments, contentWorld: contentWorld) { (value) in + result(value) + } + } else { + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + webView.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments) { (value) in + result(value) + } + } + } + else { + result(nil) + } + break + case .createPdf: + if let webView = webView, #available(macOS 11.0, *) { + let configuration = arguments!["pdfConfiguration"] as? [String: Any?] + webView.createPdf(configuration: configuration, completionHandler: { (pdf) -> Void in + result(pdf) + }) + } + else { + result(nil) + } + break + case .createWebArchiveData: + if let webView = webView, #available(macOS 11.0, *) { + webView.createWebArchiveData(dataCompletionHandler: { (webArchiveData) -> Void in + result(webArchiveData) + }) + } + else { + result(nil) + } + break + case .saveWebArchive: + if let webView = webView, #available(macOS 11.0, *) { + let filePath = arguments!["filePath"] as! String + let autoname = arguments!["autoname"] as! Bool + webView.saveWebArchive(filePath: filePath, autoname: autoname, completionHandler: { (path) -> Void in + result(path) + }) + } + else { + result(nil) + } + break + case .isSecureContext: + if let webView = webView { + webView.isSecureContext(completionHandler: { (isSecureContext) in + result(isSecureContext) + }) + } + else { + result(false) + } + break + case .createWebMessageChannel: + if let webView = webView { + let _ = webView.createWebMessageChannel { (webMessageChannel) in + result(webMessageChannel.toMap()) + } + } else { + result(nil) + } + break + case .postWebMessage: + if let webView = webView { + let message = arguments!["message"] as! [String: Any?] + let targetOrigin = arguments!["targetOrigin"] as! String + + var ports: [WebMessagePort] = [] + let portsMap = message["ports"] as? [[String: Any?]] + if let portsMap = portsMap { + for portMap in portsMap { + let webMessageChannelId = portMap["webMessageChannelId"] as! String + let index = portMap["index"] as! Int + if let webMessageChannel = webView.webMessageChannels[webMessageChannelId] { + ports.append(webMessageChannel.ports[index]) + } + } + } + let webMessage = WebMessage(data: message["data"] as? String, ports: ports) + do { + try webView.postWebMessage(message: webMessage, targetOrigin: targetOrigin) { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + } + } else { + result(false) + } + break + case .addWebMessageListener: + if let webView = webView { + let webMessageListenerMap = arguments!["webMessageListener"] as! [String: Any?] + let webMessageListener = WebMessageListener.fromMap(map: webMessageListenerMap)! + do { + try webView.addWebMessageListener(webMessageListener: webMessageListener) + result(false) + } catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + } + } else { + result(false) + } + break + case .canScrollVertically: + if let webView = webView { + result(webView.canScrollVertically()) + } else { + result(false) + } + break + case .canScrollHorizontally: + if let webView = webView { + result(webView.canScrollHorizontally()) + } else { + result(false) + } + break + case .pauseAllMediaPlayback: + if let webView = webView, #available(macOS 12.0 , *) { + webView.pauseAllMediaPlayback(completionHandler: { () -> Void in + result(true) + }) + } else { + result(false) + } + break + case .setAllMediaPlaybackSuspended: + if let webView = webView, #available(macOS 12.0 , *) { + let suspended = arguments!["suspended"] as! Bool + webView.setAllMediaPlaybackSuspended(suspended, completionHandler: { () -> Void in + result(true) + }) + } else { + result(false) + } + break + case .closeAllMediaPresentations: + if let webView = self.webView, #available(macOS 11.3, *) { + // closeAllMediaPresentations with completionHandler v15.0 makes the app crash + // with error EXC_BAD_ACCESS, so use closeAllMediaPresentations v14.5 + webView.closeAllMediaPresentations() + result(true) + } else { + result(false) + } + break + case .requestMediaPlaybackState: + if let webView = webView, #available(macOS 12.0, *) { + webView.requestMediaPlaybackState(completionHandler: { (state) -> Void in + result(state.rawValue) + }) + } else { + result(nil) + } + break + case .getMetaThemeColor: + if let webView = webView, #available(macOS 12.0, *) { + result(webView.themeColor?.hexString) + } else { + result(nil) + } + break + case .isInFullscreen: + if let webView = webView { + if #available(macOS 12.0, *) { + result(webView.fullscreenState == .inFullscreen) + } else { + result(webView.inFullscreen) + } + } + else { + result(false) + } + break + case .getCameraCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + result(webView.cameraCaptureState.rawValue) + } else { + result(nil) + } + break + case .setCameraCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + let state = WKMediaCaptureState.init(rawValue: arguments!["state"] as! Int) ?? WKMediaCaptureState.none + webView.setCameraCaptureState(state) { + result(true) + } + } else { + result(false) + } + break + case .getMicrophoneCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + result(webView.microphoneCaptureState.rawValue) + } else { + result(nil) + } + break + case .setMicrophoneCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + let state = WKMediaCaptureState.init(rawValue: arguments!["state"] as! Int) ?? WKMediaCaptureState.none + webView.setMicrophoneCaptureState(state) { + result(true) + } + } else { + result(false) + } + break + case .loadSimulatedRequest: + if let webView = webView, #available(macOS 12.0, *) { + let request = URLRequest.init(fromPluginMap: arguments!["urlRequest"] as! [String:Any?]) + let data = arguments!["data"] as! FlutterStandardTypedData + var response: URLResponse? = nil + if let urlResponse = arguments!["urlResponse"] as? [String:Any?] { + response = URLResponse.init(fromPluginMap: urlResponse) + } + if let response = response { + webView.loadSimulatedRequest(request, response: response, responseData: data.data) + } else { + webView.loadSimulatedRequest(request, responseHTML: String(decoding: data.data, as: UTF8.self)) + } + result(true) + } else { + result(false) + } + } + } + + @available(*, deprecated, message: "Use FindInteractionChannelDelegate.onFindResultReceived instead.") + public func onFindResultReceived(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Bool) { + let arguments: [String : Any?] = [ + "activeMatchOrdinal": activeMatchOrdinal, + "numberOfMatches": numberOfMatches, + "isDoneCounting": isDoneCounting + ] + channel?.invokeMethod("onFindResultReceived", arguments: arguments) + } + + public func onLongPressHitTestResult(hitTestResult: HitTestResult) { + channel?.invokeMethod("onLongPressHitTestResult", arguments: hitTestResult.toMap()) + } + + public func onScrollChanged(x: Int, y: Int) { + let arguments: [String: Any?] = ["x": x, "y": y] + channel?.invokeMethod("onScrollChanged", arguments: arguments) + } + + public func onDownloadStartRequest(request: DownloadStartRequest) { + channel?.invokeMethod("onDownloadStartRequest", arguments: request.toMap()) + } + + public func onCreateContextMenu(hitTestResult: HitTestResult) { + channel?.invokeMethod("onCreateContextMenu", arguments: hitTestResult.toMap()) + } + + public func onOverScrolled(x: Int, y: Int, clampedX: Bool, clampedY: Bool) { + let arguments: [String: Any?] = ["x": x, "y": y, "clampedX": clampedX, "clampedY": clampedY] + channel?.invokeMethod("onOverScrolled", arguments: arguments) + } + + public func onContextMenuActionItemClicked(id: Any, title: String) { + let arguments: [String: Any?] = [ + "id": id, + "iosId": id is Int64 ? String(id as! Int64) : id as! String, + "androidId": nil, + "title": title + ] + channel?.invokeMethod("onContextMenuActionItemClicked", arguments: arguments) + } + + public func onHideContextMenu() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onHideContextMenu", arguments: arguments) + } + + public func onEnterFullscreen() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onEnterFullscreen", arguments: arguments) + } + + public func onExitFullscreen() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onExitFullscreen", arguments: arguments) + } + + public class JsAlertCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsAlertResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onJsAlert(url: URL?, message: String, isMainFrame: Bool, callback: JsAlertCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsAlert", arguments: arguments, callback: callback) + } + + public class JsConfirmCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsConfirmResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onJsConfirm(url: URL?, message: String, isMainFrame: Bool, callback: JsConfirmCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsConfirm", arguments: arguments, callback: callback) + } + + public class JsPromptCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsPromptResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onJsPrompt(url: URL?, message: String, defaultValue: String?, isMainFrame: Bool, callback: JsPromptCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "defaultValue": defaultValue, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsPrompt", arguments: arguments, callback: callback) + } + + public class CreateWindowCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj is Bool && obj as! Bool + } + } + } + + public func onCreateWindow(createWindowAction: CreateWindowAction, callback: CreateWindowCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + channel?.invokeMethod("onCreateWindow", arguments: createWindowAction.toMap(), callback: callback) + } + + public func onCloseWindow() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onCloseWindow", arguments: arguments) + } + + public func onConsoleMessage(message: String, messageLevel: Int) { + let arguments: [String: Any?] = [ + "message": message, + "messageLevel": messageLevel + ] + channel?.invokeMethod("onConsoleMessage", arguments: arguments) + } + + public func onProgressChanged(progress: Int) { + let arguments: [String: Any?] = [ + "progress": progress + ] + channel?.invokeMethod("onProgressChanged", arguments: arguments) + } + + public func onTitleChanged(title: String?) { + let arguments: [String: Any?] = [ + "title": title + ] + channel?.invokeMethod("onTitleChanged", arguments: arguments) + } + + public class PermissionRequestCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return PermissionResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onPermissionRequest(request: PermissionRequest, callback: PermissionRequestCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("onPermissionRequest", arguments: request.toMap(), callback: callback); + } + + public class ShouldOverrideUrlLoadingCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return WKNavigationActionPolicy.init(rawValue: action) ?? WKNavigationActionPolicy.cancel + } + return WKNavigationActionPolicy.cancel + } + } + } + + public func shouldOverrideUrlLoading(navigationAction: WKNavigationAction, callback: ShouldOverrideUrlLoadingCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("shouldOverrideUrlLoading", arguments: navigationAction.toMap(), callback: callback); + } + + public func onLoadStart(url: String?) { + let arguments: [String: Any?] = ["url": url] + channel?.invokeMethod("onLoadStart", arguments: arguments) + } + + public func onLoadStop(url: String?) { + let arguments: [String: Any?] = ["url": url] + channel?.invokeMethod("onLoadStop", arguments: arguments) + } + + public func onUpdateVisitedHistory(url: String?, isReload: Bool?) { + let arguments: [String: Any?] = [ + "url": url, + "isReload": nil + ] + channel?.invokeMethod("onUpdateVisitedHistory", arguments: arguments) + } + + public func onReceivedError(request: WebResourceRequest, error: WebResourceError) { + let arguments: [String: Any?] = [ + "request": request.toMap(), + "error": error.toMap() + ] + channel?.invokeMethod("onReceivedError", arguments: arguments) + } + + public func onReceivedHttpError(request: WebResourceRequest, errorResponse: WebResourceResponse) { + let arguments: [String: Any?] = [ + "request": request.toMap(), + "errorResponse": errorResponse.toMap() + ] + channel?.invokeMethod("onReceivedHttpError", arguments: arguments) + } + + public class ReceivedHttpAuthRequestCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return HttpAuthResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge, callback: ReceivedHttpAuthRequestCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("onReceivedHttpAuthRequest", arguments: challenge.toMap(), callback: callback) + } + + public class ReceivedServerTrustAuthRequestCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return ServerTrustAuthResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge, callback: ReceivedServerTrustAuthRequestCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("onReceivedServerTrustAuthRequest", arguments: challenge.toMap(), callback: callback); + } + + public class ReceivedClientCertRequestCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return ClientCertResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onReceivedClientCertRequest(challenge: ClientCertChallenge, callback: ReceivedClientCertRequestCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("onReceivedClientCertRequest", arguments: challenge.toMap(), callback: callback); + } + + public func onZoomScaleChanged(newScale: Float, oldScale: Float) { + let arguments: [String: Any?] = [ + "newScale": newScale, + "oldScale": oldScale + ] + channel?.invokeMethod("onZoomScaleChanged", arguments: arguments) + } + + public func onPageCommitVisible(url: String?) { + let arguments: [String: Any?] = [ + "url": url + ] + channel?.invokeMethod("onPageCommitVisible", arguments: arguments) + } + + public class LoadResourceWithCustomSchemeCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return CustomSchemeResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onLoadResourceWithCustomScheme(request: WebResourceRequest, callback: LoadResourceWithCustomSchemeCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = ["request": request.toMap()] + channel.invokeMethod("onLoadResourceWithCustomScheme", arguments: arguments, callback: callback) + } + + public class CallJsHandlerCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj + } + } + } + + public func onCallJsHandler(handlerName: String, args: String, callback: CallJsHandlerCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "handlerName": handlerName, + "args": args + ] + channel.invokeMethod("onCallJsHandler", arguments: arguments, callback: callback); + } + + public class NavigationResponseCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return WKNavigationResponsePolicy.init(rawValue: action) ?? WKNavigationResponsePolicy.cancel + } + return WKNavigationResponsePolicy.cancel + } + } + } + + public func onNavigationResponse(navigationResponse: WKNavigationResponse, callback: NavigationResponseCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("onNavigationResponse", arguments: navigationResponse.toMap(), callback: callback); + } + + public class ShouldAllowDeprecatedTLSCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return action == 1 + } + return false + } + } + } + + public func shouldAllowDeprecatedTLS(challenge: URLAuthenticationChallenge, callback: ShouldAllowDeprecatedTLSCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + channel.invokeMethod("shouldAllowDeprecatedTLS", arguments: challenge.toMap(), callback: callback) + } + + public func onWebContentProcessDidTerminate() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onWebContentProcessDidTerminate", arguments: arguments) + } + + public func onDidReceiveServerRedirectForProvisionalNavigation() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onDidReceiveServerRedirectForProvisionalNavigation", arguments: arguments) + } + + @available(macOS 12.0, *) + public func onCameraCaptureStateChanged(oldState: WKMediaCaptureState?, newState: WKMediaCaptureState?) { + let arguments = [ + "oldState": oldState?.rawValue, + "newState": newState?.rawValue + ] + channel?.invokeMethod("onCameraCaptureStateChanged", arguments: arguments) + } + + @available(macOS 12.0, *) + public func onMicrophoneCaptureStateChanged(oldState: WKMediaCaptureState?, newState: WKMediaCaptureState?) { + let arguments = [ + "oldState": oldState?.rawValue, + "newState": newState?.rawValue + ] + channel?.invokeMethod("onMicrophoneCaptureStateChanged", arguments: arguments) + } + + public class PrintRequestCallback : BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj is Bool && obj as! Bool + } + } + } + + public func onPrintRequest(url: URL?, printJobId: String?, callback: PrintRequestCallback) { + guard let channel = channel else { + callback.defaultBehaviour(nil) + return + } + let arguments = [ + "url": url?.absoluteString, + "printJobId": printJobId, + ] + channel.invokeMethod("onPrintRequest", arguments: arguments, callback: callback) + } + + public override func dispose() { + super.dispose() + webView = nil + } + + deinit { + debugPrint("WebViewChannelDelegate - dealloc") + dispose() + } +} diff --git a/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift b/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift new file mode 100644 index 00000000..da6dba91 --- /dev/null +++ b/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift @@ -0,0 +1,84 @@ +// +// WebViewChannelDelegateMethods.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/10/22. +// + +import Foundation + +public enum WebViewChannelDelegateMethods: String { + case getUrl = "getUrl" + case getTitle = "getTitle" + case getProgress = "getProgress" + case loadUrl = "loadUrl" + case postUrl = "postUrl" + case loadData = "loadData" + case loadFile = "loadFile" + case evaluateJavascript = "evaluateJavascript" + case injectJavascriptFileFromUrl = "injectJavascriptFileFromUrl" + case injectCSSCode = "injectCSSCode" + case injectCSSFileFromUrl = "injectCSSFileFromUrl" + case reload = "reload" + case goBack = "goBack" + case canGoBack = "canGoBack" + case goForward = "goForward" + case canGoForward = "canGoForward" + case goBackOrForward = "goBackOrForward" + case canGoBackOrForward = "canGoBackOrForward" + case stopLoading = "stopLoading" + case isLoading = "isLoading" + case takeScreenshot = "takeScreenshot" + case setSettings = "setSettings" + case getSettings = "getSettings" + case close = "close" + case show = "show" + case hide = "hide" + case getCopyBackForwardList = "getCopyBackForwardList" + case clearCache = "clearCache" + case scrollTo = "scrollTo" + case scrollBy = "scrollBy" + case pauseTimers = "pauseTimers" + case resumeTimers = "resumeTimers" + case printCurrentPage = "printCurrentPage" + case getContentHeight = "getContentHeight" + case zoomBy = "zoomBy" + case reloadFromOrigin = "reloadFromOrigin" + case getOriginalUrl = "getOriginalUrl" + case getZoomScale = "getZoomScale" + case hasOnlySecureContent = "hasOnlySecureContent" + case getSelectedText = "getSelectedText" + case getHitTestResult = "getHitTestResult" + case clearFocus = "clearFocus" + case setContextMenu = "setContextMenu" + case requestFocusNodeHref = "requestFocusNodeHref" + case requestImageRef = "requestImageRef" + case getScrollX = "getScrollX" + case getScrollY = "getScrollY" + case getCertificate = "getCertificate" + case addUserScript = "addUserScript" + case removeUserScript = "removeUserScript" + case removeUserScriptsByGroupName = "removeUserScriptsByGroupName" + case removeAllUserScripts = "removeAllUserScripts" + case callAsyncJavaScript = "callAsyncJavaScript" + case createPdf = "createPdf" + case createWebArchiveData = "createWebArchiveData" + case saveWebArchive = "saveWebArchive" + case isSecureContext = "isSecureContext" + case createWebMessageChannel = "createWebMessageChannel" + case postWebMessage = "postWebMessage" + case addWebMessageListener = "addWebMessageListener" + case canScrollVertically = "canScrollVertically" + case canScrollHorizontally = "canScrollHorizontally" + case pauseAllMediaPlayback = "pauseAllMediaPlayback" + case setAllMediaPlaybackSuspended = "setAllMediaPlaybackSuspended" + case closeAllMediaPresentations = "closeAllMediaPresentations" + case requestMediaPlaybackState = "requestMediaPlaybackState" + case getMetaThemeColor = "getMetaThemeColor" + case isInFullscreen = "isInFullscreen" + case getCameraCaptureState = "getCameraCaptureState" + case setCameraCaptureState = "setCameraCaptureState" + case getMicrophoneCaptureState = "getMicrophoneCaptureState" + case setMicrophoneCaptureState = "setMicrophoneCaptureState" + case loadSimulatedRequest = "loadSimulatedRequest" +} diff --git a/macos/Classes/InAppWebViewFlutterPlugin.swift b/macos/Classes/InAppWebViewFlutterPlugin.swift new file mode 100644 index 00000000..c0c8043e --- /dev/null +++ b/macos/Classes/InAppWebViewFlutterPlugin.swift @@ -0,0 +1,8 @@ +import Cocoa +import FlutterMacOS + +public class InAppWebViewFlutterPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + SwiftFlutterPlugin.register(with: registrar) + } +} diff --git a/macos/Classes/InAppWebViewStatic.swift b/macos/Classes/InAppWebViewStatic.swift new file mode 100755 index 00000000..8d28d1db --- /dev/null +++ b/macos/Classes/InAppWebViewStatic.swift @@ -0,0 +1,81 @@ +// +// InAppWebViewStatic.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/12/2019. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class InAppWebViewStatic: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_static" + static var registrar: FlutterPluginRegistrar? + static var webViewForUserAgent: WKWebView? + static var defaultUserAgent: String? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: InAppWebViewStatic.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + InAppWebViewStatic.registrar = registrar + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getDefaultUserAgent": + InAppWebViewStatic.getDefaultUserAgent(completionHandler: { (value) in + result(value) + }) + break + case "handlesURLScheme": + let urlScheme = arguments!["urlScheme"] as! String + if #available(macOS 10.13, *) { + result(WKWebView.handlesURLScheme(urlScheme)) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + static public func getDefaultUserAgent(completionHandler: @escaping (_ value: String?) -> Void) { + if defaultUserAgent == nil { + InAppWebViewStatic.webViewForUserAgent = WKWebView() + InAppWebViewStatic.webViewForUserAgent?.evaluateJavaScript("navigator.userAgent") { (value, error) in + + if error != nil { + print("Error occured to get userAgent") + self.webViewForUserAgent = nil + completionHandler(nil) + return + } + + if let unwrappedUserAgent = value as? String { + InAppWebViewStatic.defaultUserAgent = unwrappedUserAgent + completionHandler(defaultUserAgent) + } else { + print("Failed to get userAgent") + } + self.webViewForUserAgent = nil + } + } else { + completionHandler(defaultUserAgent) + } + } + + public override func dispose() { + super.dispose() + InAppWebViewStatic.registrar = nil + InAppWebViewStatic.webViewForUserAgent = nil + InAppWebViewStatic.defaultUserAgent = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/LeakAvoider.swift b/macos/Classes/LeakAvoider.swift new file mode 100755 index 00000000..9f447142 --- /dev/null +++ b/macos/Classes/LeakAvoider.swift @@ -0,0 +1,26 @@ +// +// LeakAvoider.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/12/2019. +// + +import Foundation +import FlutterMacOS + +public class LeakAvoider: NSObject { + weak var delegate : FlutterMethodCallDelegate? + + init(delegate: FlutterMethodCallDelegate) { + super.init() + self.delegate = delegate + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + self.delegate?.handle(call, result: result) + } + + deinit { + debugPrint("LeakAvoider - dealloc") + } +} diff --git a/macos/Classes/MyCookieManager.swift b/macos/Classes/MyCookieManager.swift new file mode 100755 index 00000000..f42f83d3 --- /dev/null +++ b/macos/Classes/MyCookieManager.swift @@ -0,0 +1,309 @@ +// +// MyCookieManager.swift +// flutter_inappwebview +// +// Created by Lorenzo on 26/10/18. +// + +import Foundation +import WebKit +import FlutterMacOS + +@available(macOS 10.13, *) +public class MyCookieManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_cookiemanager" + static var registrar: FlutterPluginRegistrar? + static var httpCookieStore: WKHTTPCookieStore? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: MyCookieManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + MyCookieManager.registrar = registrar + MyCookieManager.httpCookieStore = WKWebsiteDataStore.default().httpCookieStore + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + switch call.method { + case "setCookie": + let url = arguments!["url"] as! String + let name = arguments!["name"] as! String + let value = arguments!["value"] as! String + let path = arguments!["path"] as! String + + var expiresDate: Int64? + if let expiresDateString = arguments!["expiresDate"] as? String { + expiresDate = Int64(expiresDateString) + } + + let maxAge = arguments!["maxAge"] as? Int64 + let isSecure = arguments!["isSecure"] as? Bool + let isHttpOnly = arguments!["isHttpOnly"] as? Bool + let sameSite = arguments!["sameSite"] as? String + let domain = arguments!["domain"] as? String + + MyCookieManager.setCookie(url: url, + name: name, + value: value, + path: path, + domain: domain, + expiresDate: expiresDate, + maxAge: maxAge, + isSecure: isSecure, + isHttpOnly: isHttpOnly, + sameSite: sameSite, + result: result) + break + case "getCookies": + let url = arguments!["url"] as! String + MyCookieManager.getCookies(url: url, result: result) + break + case "getAllCookies": + MyCookieManager.getAllCookies(result: result) + break + case "deleteCookie": + let url = arguments!["url"] as! String + let name = arguments!["name"] as! String + let path = arguments!["path"] as! String + let domain = arguments!["domain"] as? String + MyCookieManager.deleteCookie(url: url, name: name, path: path, domain: domain, result: result) + break; + case "deleteCookies": + let url = arguments!["url"] as! String + let path = arguments!["path"] as! String + let domain = arguments!["domain"] as? String + MyCookieManager.deleteCookies(url: url, path: path, domain: domain, result: result) + break; + case "deleteAllCookies": + MyCookieManager.deleteAllCookies(result: result) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public static func setCookie(url: String, + name: String, + value: String, + path: String, + domain: String?, + expiresDate: Int64?, + maxAge: Int64?, + isSecure: Bool?, + isHttpOnly: Bool?, + sameSite: String?, + result: @escaping FlutterResult) { + guard let httpCookieStore = MyCookieManager.httpCookieStore else { + result(false) + return + } + + var properties: [HTTPCookiePropertyKey: Any] = [:] + properties[.originURL] = url + properties[.name] = name + properties[.value] = value + properties[.path] = path + + if domain != nil { + properties[.domain] = domain + } + + if expiresDate != nil { + // convert from milliseconds + properties[.expires] = Date(timeIntervalSince1970: TimeInterval(Double(expiresDate!)/1000)) + } + if maxAge != nil { + properties[.maximumAge] = String(maxAge!) + } + if isSecure != nil && isSecure! { + properties[.secure] = "TRUE" + } + if isHttpOnly != nil && isHttpOnly! { + properties[.init("HttpOnly")] = "YES" + } + if sameSite != nil { + if #available(macOS 10.15, *) { + var sameSiteValue = HTTPCookieStringPolicy(rawValue: "None") + switch sameSite { + case "Lax": + sameSiteValue = HTTPCookieStringPolicy.sameSiteLax + case "Strict": + sameSiteValue = HTTPCookieStringPolicy.sameSiteStrict + default: + break + } + properties[.sameSitePolicy] = sameSiteValue + } else { + properties[.init("SameSite")] = sameSite + } + } + + let cookie = HTTPCookie(properties: properties)! + + httpCookieStore.setCookie(cookie, completionHandler: {() in + result(true) + }) + } + + public static func getCookies(url: String, result: @escaping FlutterResult) { + var cookieList: [[String: Any?]] = [] + + guard let httpCookieStore = MyCookieManager.httpCookieStore else { + result(cookieList) + return + } + + if let urlHost = URL(string: url)?.host { + httpCookieStore.getAllCookies { (cookies) in + for cookie in cookies { + if urlHost.hasSuffix(cookie.domain) || ".\(urlHost)".hasSuffix(cookie.domain) { + var sameSite: String? = nil + if #available(macOS 10.15, *) { + if let sameSiteValue = cookie.sameSitePolicy?.rawValue { + sameSite = sameSiteValue.prefix(1).capitalized + sameSiteValue.dropFirst() + } + } + + var expiresDateTimestamp: Int64 = -1 + if let expiresDate = cookie.expiresDate?.timeIntervalSince1970 { + // convert to milliseconds + expiresDateTimestamp = Int64(expiresDate * 1000) + } + + cookieList.append([ + "name": cookie.name, + "value": cookie.value, + "expiresDate": expiresDateTimestamp != -1 ? expiresDateTimestamp : nil, + "isSessionOnly": cookie.isSessionOnly, + "domain": cookie.domain, + "sameSite": sameSite, + "isSecure": cookie.isSecure, + "isHttpOnly": cookie.isHTTPOnly, + "path": cookie.path, + ]) + } + } + result(cookieList) + } + return + } else { + print("Cannot get WebView cookies. No HOST found for URL: \(url)") + } + + result(cookieList) + } + + public static func getAllCookies(result: @escaping FlutterResult) { + var cookieList: [[String: Any?]] = [] + + guard let httpCookieStore = MyCookieManager.httpCookieStore else { + result(cookieList) + return + } + + httpCookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var sameSite: String? = nil + if #available(macOS 10.15, *) { + if let sameSiteValue = cookie.sameSitePolicy?.rawValue { + sameSite = sameSiteValue.prefix(1).capitalized + sameSiteValue.dropFirst() + } + } + + var expiresDateTimestamp: Int64 = -1 + if let expiresDate = cookie.expiresDate?.timeIntervalSince1970 { + // convert to milliseconds + expiresDateTimestamp = Int64(expiresDate * 1000) + } + + cookieList.append([ + "name": cookie.name, + "value": cookie.value, + "expiresDate": expiresDateTimestamp != -1 ? expiresDateTimestamp : nil, + "isSessionOnly": cookie.isSessionOnly, + "domain": cookie.domain, + "sameSite": sameSite, + "isSecure": cookie.isSecure, + "isHttpOnly": cookie.isHTTPOnly, + "path": cookie.path, + ]) + } + result(cookieList) + } + } + + public static func deleteCookie(url: String, name: String, path: String, domain: String?, result: @escaping FlutterResult) { + guard let httpCookieStore = MyCookieManager.httpCookieStore else { + result(false) + return + } + + var domain = domain + httpCookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var originURL = url + if cookie.properties![.originURL] is String { + originURL = cookie.properties![.originURL] as! String + } + else if cookie.properties![.originURL] is URL { + originURL = (cookie.properties![.originURL] as! URL).absoluteString + } + if domain == nil, let domainUrl = URL(string: originURL) { + domain = domainUrl.host + } + if let domain = domain, cookie.domain == domain, cookie.name == name, cookie.path == path { + httpCookieStore.delete(cookie, completionHandler: { + result(true) + }) + return + } + } + result(false) + } + } + + public static func deleteCookies(url: String, path: String, domain: String?, result: @escaping FlutterResult) { + guard let httpCookieStore = MyCookieManager.httpCookieStore else { + result(false) + return + } + + var domain = domain + httpCookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var originURL = url + if cookie.properties![.originURL] is String { + originURL = cookie.properties![.originURL] as! String + } + else if cookie.properties![.originURL] is URL { + originURL = (cookie.properties![.originURL] as! URL).absoluteString + } + if domain == nil, let domainUrl = URL(string: originURL) { + domain = domainUrl.host + } + if let domain = domain, cookie.domain == domain, cookie.path == path { + httpCookieStore.delete(cookie, completionHandler: nil) + } + } + result(true) + } + } + + public static func deleteAllCookies(result: @escaping FlutterResult) { + let websiteDataTypes = NSSet(array: [WKWebsiteDataTypeCookies]) + let date = NSDate(timeIntervalSince1970: 0) + WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes as! Set, modifiedSince: date as Date, completionHandler:{ + result(true) + }) + } + + public override func dispose() { + super.dispose() + MyCookieManager.registrar = nil + MyCookieManager.httpCookieStore = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/MyWebStorageManager.swift b/macos/Classes/MyWebStorageManager.swift new file mode 100755 index 00000000..274a8c3f --- /dev/null +++ b/macos/Classes/MyWebStorageManager.swift @@ -0,0 +1,112 @@ +// +// MyWebStorageManager.swift +// connectivity +// +// Created by Lorenzo Pichilli on 16/12/2019. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class MyWebStorageManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webstoragemanager" + static var registrar: FlutterPluginRegistrar? + static var websiteDataStore: WKWebsiteDataStore? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: MyWebStorageManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + MyWebStorageManager.registrar = registrar + MyWebStorageManager.websiteDataStore = WKWebsiteDataStore.default() + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + switch call.method { + case "fetchDataRecords": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + MyWebStorageManager.fetchDataRecords(dataTypes: dataTypes, result: result) + break + case "removeDataFor": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + let recordList = arguments!["recordList"] as! [[String: Any?]] + MyWebStorageManager.removeDataFor(dataTypes: dataTypes, recordList: recordList, result: result) + break + case "removeDataModifiedSince": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + let timestamp = arguments!["timestamp"] as! Int64 + MyWebStorageManager.removeDataModifiedSince(dataTypes: dataTypes, timestamp: timestamp, result: result) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public static func fetchDataRecords(dataTypes: Set, result: @escaping FlutterResult) { + var recordList: [[String: Any?]] = [] + + guard let websiteDataStore = MyWebStorageManager.websiteDataStore else { + result(recordList) + return + } + + websiteDataStore.fetchDataRecords(ofTypes: dataTypes) { (data) in + for record in data { + recordList.append([ + "displayName": record.displayName, + "dataTypes": record.dataTypes.map({ (dataType) -> String in + return dataType + }) + ]) + } + result(recordList) + } + } + + public static func removeDataFor(dataTypes: Set, recordList: [[String: Any?]], result: @escaping FlutterResult) { + var records: [WKWebsiteDataRecord] = [] + + guard let websiteDataStore = MyWebStorageManager.websiteDataStore else { + result(false) + return + } + + websiteDataStore.fetchDataRecords(ofTypes: dataTypes) { (data) in + for record in data { + for r in recordList { + let displayName = r["displayName"] as! String + if (record.displayName == displayName) { + records.append(record) + break + } + } + } + websiteDataStore.removeData(ofTypes: dataTypes, for: records) { + result(true) + } + } + } + + public static func removeDataModifiedSince(dataTypes: Set, timestamp: Int64, result: @escaping FlutterResult) { + guard let websiteDataStore = MyWebStorageManager.websiteDataStore else { + result(false) + return + } + + let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp)) + websiteDataStore.removeData(ofTypes: dataTypes, modifiedSince: date as Date) { + result(true) + } + } + + public override func dispose() { + super.dispose() + MyWebStorageManager.registrar = nil + MyWebStorageManager.websiteDataStore = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/PlatformUtil.swift b/macos/Classes/PlatformUtil.swift new file mode 100644 index 00000000..cdd3c62e --- /dev/null +++ b/macos/Classes/PlatformUtil.swift @@ -0,0 +1,66 @@ +// +// PlatformUtil.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/03/21. +// + +import Foundation +import FlutterMacOS + +public class PlatformUtil: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_platformutil" + static var registrar: FlutterPluginRegistrar? + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: PlatformUtil.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + InAppWebViewStatic.registrar = registrar + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getSystemVersion": + result(ProcessInfo.processInfo.operatingSystemVersionString) + break + case "formatDate": + let date = arguments!["date"] as! Int64 + let format = arguments!["format"] as! String + let locale = PlatformUtil.getLocaleFromString(locale: arguments!["locale"] as? String) + let timezone = TimeZone.init(abbreviation: arguments!["timezone"] as? String ?? "UTC")! + result(PlatformUtil.formatDate(date: date, format: format, locale: locale, timezone: timezone)) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + static public func getLocaleFromString(locale: String?) -> Locale { + guard let locale = locale else { + return Locale.init(identifier: "en_US") + } + return Locale.init(identifier: locale) + } + + static public func getDateFromMilliseconds(date: Int64) -> Date { + return Date(timeIntervalSince1970: TimeInterval(Double(date)/1000)) + } + + static public func formatDate(date: Int64, format: String, locale: Locale, timezone: TimeZone) -> String { + let formatter = DateFormatter() + formatter.dateFormat = format + formatter.timeZone = timezone + return formatter.string(from: PlatformUtil.getDateFromMilliseconds(date: date)) + } + + public override func dispose() { + super.dispose() + PlatformUtil.registrar = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift b/macos/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift new file mode 100644 index 00000000..5622691a --- /dev/null +++ b/macos/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift @@ -0,0 +1,21 @@ +// +// CallAsyncJavaScriptBelowIOS14WrapperJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS = """ +(function(obj) { + (async function(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES) { + \(PluginScriptsUtil.VAR_FUNCTION_BODY) + })(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES)).then(function(value) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': value, 'error': null, 'resultUuid': '\(PluginScriptsUtil.VAR_RESULT_UUID)'}); + }).catch(function(error) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': null, 'error': error + '', 'resultUuid': '\(PluginScriptsUtil.VAR_RESULT_UUID)'}); + }); + return null; +})(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ)); +""" diff --git a/macos/Classes/PluginScriptsJS/ConsoleLogJS.swift b/macos/Classes/PluginScriptsJS/ConsoleLogJS.swift new file mode 100644 index 00000000..69a92953 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/ConsoleLogJS.swift @@ -0,0 +1,58 @@ +// +// ConsoleLogJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_CONSOLE_LOG_JS_PLUGIN_SCRIPT" + +let CONSOLE_LOG_JS_PLUGIN_SCRIPT = PluginScript( + groupName: CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: CONSOLE_LOG_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: ["consoleLog", "consoleDebug", "consoleError", "consoleInfo", "consoleWarn"]) + +// the message needs to be concatenated with '' in order to have the same behavior like on Android +let CONSOLE_LOG_JS_SOURCE = """ +(function(console) { + + var oldLogs = { + 'consoleLog': console.log, + 'consoleDebug': console.debug, + 'consoleError': console.error, + 'consoleInfo': console.info, + 'consoleWarn': console.warn + }; + + for (var k in oldLogs) { + (function(oldLog) { + console[oldLog.replace('console', '').toLowerCase()] = function() { + oldLogs[oldLog].apply(null, arguments); + var args = arguments; + + // on iOS, for some reason, accessing the arguments object synchronously can throw some errors, such as "TypeError" + // see https://github.com/pichillilorenzo/flutter_inappwebview/issues/776 + setTimeout(function() { + var message = ''; + for (var i in args) { + if (message == '') { + message += args[i]; + } + else { + message += ' ' + args[i]; + } + } + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + window.webkit.messageHandlers[oldLog].postMessage({'message': message, '_windowId': _windowId}); + }); + } + })(k); + } +})(window.console); +""" diff --git a/macos/Classes/PluginScriptsJS/EnableViewportScaleJS.swift b/macos/Classes/PluginScriptsJS/EnableViewportScaleJS.swift new file mode 100644 index 00000000..593ba00e --- /dev/null +++ b/macos/Classes/PluginScriptsJS/EnableViewportScaleJS.swift @@ -0,0 +1,36 @@ +// +// EnableViewportScaleJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT" + +let ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ENABLE_VIEWPORT_SCALE_JS_SOURCE, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ENABLE_VIEWPORT_SCALE_JS_SOURCE = """ +(function() { + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', 'width=device-width'); + document.getElementsByTagName('head')[0].appendChild(meta); +})() +""" + +let NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE = """ +(function() { + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent); + document.getElementsByTagName('head')[0].appendChild(meta); +})() +""" diff --git a/macos/Classes/PluginScriptsJS/FindElementsAtPointJS.swift b/macos/Classes/PluginScriptsJS/FindElementsAtPointJS.swift new file mode 100644 index 00000000..b3c22282 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/FindElementsAtPointJS.swift @@ -0,0 +1,73 @@ +// +// FindElementsAtPointJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT" + +let FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: FIND_ELEMENTS_AT_POINT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +/** + https://developer.android.com/reference/android/webkit/WebView.HitTestResult + */ +let FIND_ELEMENTS_AT_POINT_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint = function(x, y) { + var hitTestResultType = { + UNKNOWN_TYPE: 0, + PHONE_TYPE: 2, + GEO_TYPE: 3, + EMAIL_TYPE: 4, + IMAGE_TYPE: 5, + SRC_ANCHOR_TYPE: 7, + SRC_IMAGE_ANCHOR_TYPE: 8, + EDIT_TEXT_TYPE: 9 + }; + var element = document.elementFromPoint(x, y); + var data = { + type: 0, + extra: null + }; + while (element) { + if (element.tagName === 'IMG' && element.src) { + if (element.parentNode && element.parentNode.tagName === 'A' && element.parentNode.href) { + data.type = hitTestResultType.SRC_IMAGE_ANCHOR_TYPE; + } else { + data.type = hitTestResultType.IMAGE_TYPE; + } + data.extra = element.src; + break; + } else if (element.tagName === 'A' && element.href) { + if (element.href.indexOf('mailto:') === 0) { + data.type = hitTestResultType.EMAIL_TYPE; + data.extra = element.href.replace('mailto:', ''); + } else if (element.href.indexOf('tel:') === 0) { + data.type = hitTestResultType.PHONE_TYPE; + data.extra = element.href.replace('tel:', ''); + } else if (element.href.indexOf('geo:') === 0) { + data.type = hitTestResultType.GEO_TYPE; + data.extra = element.href.replace('geo:', ''); + } else { + data.type = hitTestResultType.SRC_ANCHOR_TYPE; + data.extra = element.href; + } + break; + } else if ( + (element.tagName === 'INPUT' && ['text', 'email', 'password', 'number', 'search', 'tel', 'url'].indexOf(element.type) >= 0) || + element.tagName === 'TEXTAREA') { + data.type = hitTestResultType.EDIT_TEXT_TYPE + } + element = element.parentNode; + } + return data; +} +""" diff --git a/macos/Classes/PluginScriptsJS/FindTextHighlightJS.swift b/macos/Classes/PluginScriptsJS/FindTextHighlightJS.swift new file mode 100644 index 00000000..a61a05b7 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/FindTextHighlightJS.swift @@ -0,0 +1,187 @@ +// +// FindTextHighlightJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT" +let FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._searchResultCount" +let FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._currentHighlight" +let FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._isDoneCounting" + +let FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: FIND_TEXT_HIGHLIGHT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onFindResultReceived"]) + +let FIND_TEXT_HIGHLIGHT_JS_SOURCE = """ +\(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) = 0; +\(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = 0; +\(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = false; +window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement = function(element, keyword) { + if (element) { + if (element.nodeType == 3) { + // Text node + + var elementTmp = element; + while (true) { + var value = elementTmp.nodeValue; // Search for keyword in text node + var idx = value.toLowerCase().indexOf(keyword); + + if (idx < 0) break; + + var span = document.createElement("span"); + var text = document.createTextNode(value.substr(idx, keyword.length)); + span.appendChild(text); + + span.setAttribute( + "id", + "\(JAVASCRIPT_BRIDGE_NAME)_SEARCH_WORD_" + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + ); + span.setAttribute("class", "\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); + var backgroundColor = \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) == 0 ? "#FF9732" : "#FFFF00"; + span.setAttribute("style", "color: #000 !important; background: " + backgroundColor + " !important; padding: 0px !important; margin: 0px !important; border: 0px !important;"); + + text = document.createTextNode(value.substr(idx + keyword.length)); + element.deleteData(idx, value.length - idx); + + var next = element.nextSibling; + element.parentNode.insertBefore(span, next); + element.parentNode.insertBefore(text, next); + element = text; + + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE)++; + elementTmp = document.createTextNode( + value.substr(idx + keyword.length) + ); + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); + } + } else if (element.nodeType == 1) { + // Element node + if ( + element.style.display != "none" && + element.nodeName.toLowerCase() != "select" + ) { + for (var i = element.childNodes.length - 1; i >= 0; i--) { + window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement( + element.childNodes[element.childNodes.length - 1 - i], + keyword + ); + } + } + } + } +} + +// the main entry point to start the search +window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync = function(keyword) { + window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches(); + window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement(document.body, keyword.toLowerCase()); + \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = true; + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); +} + +// helper function, recursively removes the highlights in elements and their childs +window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement = function(element) { + if (element) { + if (element.nodeType == 1) { + if (element.getAttribute("class") == "\(JAVASCRIPT_BRIDGE_NAME)_Highlight") { + var text = element.removeChild(element.firstChild); + element.parentNode.insertBefore(text, element); + element.parentNode.removeChild(element); + return true; + } else { + var normalize = false; + for (var i = element.childNodes.length - 1; i >= 0; i--) { + if (window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement(element.childNodes[i])) { + normalize = true; + } + } + if (normalize) { + element.normalize(); + } + } + } + } + return false; +} + +// the main entry point to remove the highlights +window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches = function() { + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) = 0; + \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = 0; + \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = false; + window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement(document.body); +} + +window.\(JAVASCRIPT_BRIDGE_NAME)._findNext = function(forward) { + if (\(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) <= 0) return; + + var idx = \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) + (forward ? +1 : -1); + idx = + idx < 0 + ? \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) - 1 + : idx >= \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + ? 0 + : idx; + \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = idx; + + var scrollTo = document.getElementById("\(JAVASCRIPT_BRIDGE_NAME)_SEARCH_WORD_" + idx); + if (scrollTo) { + var highlights = document.getElementsByClassName("\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); + for (var i = 0; i < highlights.length; i++) { + var span = highlights[i]; + span.style.backgroundColor = "#FFFF00"; + } + scrollTo.style.backgroundColor = "#FF9732"; + + scrollTo.scrollIntoView({ + behavior: "auto", + block: "center" + }); + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); + } +} +""" diff --git a/macos/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift b/macos/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift new file mode 100644 index 00000000..b5d3a54f --- /dev/null +++ b/macos/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift @@ -0,0 +1,251 @@ +// +// InterceptAjaxRequestsJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useShouldInterceptAjaxRequest" + +let INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT = PluginScript( + groupName: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: INTERCEPT_AJAX_REQUEST_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let INTERCEPT_AJAX_REQUEST_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) = true; +(function(ajax) { + var send = ajax.prototype.send; + var open = ajax.prototype.open; + var setRequestHeader = ajax.prototype.setRequestHeader; + ajax.prototype._flutter_inappwebview_url = null; + ajax.prototype._flutter_inappwebview_method = null; + ajax.prototype._flutter_inappwebview_isAsync = null; + ajax.prototype._flutter_inappwebview_user = null; + ajax.prototype._flutter_inappwebview_password = null; + ajax.prototype._flutter_inappwebview_password = null; + ajax.prototype._flutter_inappwebview_already_onreadystatechange_wrapped = false; + ajax.prototype._flutter_inappwebview_request_headers = {}; + function convertRequestResponse(request, callback) { + if (request.response != null && request.responseType != null) { + switch (request.responseType) { + case 'arraybuffer': + callback(new Uint8Array(request.response)); + return; + case 'blob': + const reader = new FileReader(); + reader.addEventListener('loadend', function() { + callback(new Uint8Array(reader.result)); + }); + reader.readAsArrayBuffer(blob); + return; + case 'document': + callback(request.response.documentElement.outerHTML); + return; + case 'json': + callback(request.response); + return; + }; + } + callback(null); + }; + ajax.prototype.open = function(method, url, isAsync, user, password) { + isAsync = (isAsync != null) ? isAsync : true; + this._flutter_inappwebview_url = url; + this._flutter_inappwebview_method = method; + this._flutter_inappwebview_isAsync = isAsync; + this._flutter_inappwebview_user = user; + this._flutter_inappwebview_password = password; + this._flutter_inappwebview_request_headers = {}; + open.call(this, method, url, isAsync, user, password); + }; + ajax.prototype.setRequestHeader = function(header, value) { + this._flutter_inappwebview_request_headers[header] = value; + setRequestHeader.call(this, header, value); + }; + function handleEvent(e) { + var self = this; + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == true) { + var headers = this.getAllResponseHeaders(); + var responseHeaders = {}; + if (headers != null) { + var arr = headers.trim().split(/[\\r\\n]+/); + arr.forEach(function (line) { + var parts = line.split(': '); + var header = parts.shift(); + var value = parts.join(': '); + responseHeaders[header] = value; + }); + } + convertRequestResponse(this, function(response) { + var ajaxRequest = { + method: self._flutter_inappwebview_method, + url: self._flutter_inappwebview_url, + isAsync: self._flutter_inappwebview_isAsync, + user: self._flutter_inappwebview_user, + password: self._flutter_inappwebview_password, + withCredentials: self.withCredentials, + headers: self._flutter_inappwebview_request_headers, + readyState: self.readyState, + status: self.status, + responseURL: self.responseURL, + responseType: self.responseType, + response: response, + responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null, + responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null, + statusText: self.statusText, + responseHeaders, responseHeaders, + event: { + type: e.type, + loaded: e.loaded, + lengthComputable: e.lengthComputable, + total: e.total + } + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onAjaxProgress', ajaxRequest).then(function(result) { + if (result != null) { + switch (result) { + case 0: + self.abort(); + return; + }; + } + }); + }); + } + }; + ajax.prototype.send = function(data) { + var self = this; + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == true) { + if (!this._flutter_inappwebview_already_onreadystatechange_wrapped) { + this._flutter_inappwebview_already_onreadystatechange_wrapped = true; + var onreadystatechange = this.onreadystatechange; + this.onreadystatechange = function() { + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == true) { + var headers = this.getAllResponseHeaders(); + var responseHeaders = {}; + if (headers != null) { + var arr = headers.trim().split(/[\\r\\n]+/); + arr.forEach(function (line) { + var parts = line.split(': '); + var header = parts.shift(); + var value = parts.join(': '); + responseHeaders[header] = value; + }); + } + convertRequestResponse(this, function(response) { + var ajaxRequest = { + method: self._flutter_inappwebview_method, + url: self._flutter_inappwebview_url, + isAsync: self._flutter_inappwebview_isAsync, + user: self._flutter_inappwebview_user, + password: self._flutter_inappwebview_password, + withCredentials: self.withCredentials, + headers: self._flutter_inappwebview_request_headers, + readyState: self.readyState, + status: self.status, + responseURL: self.responseURL, + responseType: self.responseType, + response: response, + responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null, + responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null, + statusText: self.statusText, + responseHeaders: responseHeaders + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onAjaxReadyStateChange', ajaxRequest).then(function(result) { + if (result != null) { + switch (result) { + case 0: + self.abort(); + return; + }; + } + if (onreadystatechange != null) { + onreadystatechange(); + } + }); + }); + } else if (onreadystatechange != null) { + onreadystatechange(); + } + }; + } + this.addEventListener('loadstart', handleEvent); + this.addEventListener('load', handleEvent); + this.addEventListener('loadend', handleEvent); + this.addEventListener('progress', handleEvent); + this.addEventListener('error', handleEvent); + this.addEventListener('abort', handleEvent); + this.addEventListener('timeout', handleEvent); + \(JAVASCRIPT_UTIL_VAR_NAME).convertBodyRequest(data).then(function(data) { + var ajaxRequest = { + data: data, + method: self._flutter_inappwebview_method, + url: self._flutter_inappwebview_url, + isAsync: self._flutter_inappwebview_isAsync, + user: self._flutter_inappwebview_user, + password: self._flutter_inappwebview_password, + withCredentials: self.withCredentials, + headers: self._flutter_inappwebview_request_headers, + responseType: self.responseType + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) { + if (result != null) { + switch (result) { + case 0: + self.abort(); + return; + }; + if (result.data != null && !\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.data) && result.data.length > 0) { + var bodyString = \(JAVASCRIPT_UTIL_VAR_NAME).arrayBufferToString(result.data); + if (\(JAVASCRIPT_UTIL_VAR_NAME).isBodyFormData(bodyString)) { + var formDataContentType = \(JAVASCRIPT_UTIL_VAR_NAME).getFormDataContentType(bodyString); + if (result.headers != null) { + result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type']; + } else { + result.headers = { 'Content-Type': formDataContentType }; + } + } + } + if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.data) || result.data == null) { + data = result.data; + } else if (result.data.length > 0) { + data = new Uint8Array(result.data); + } + self.withCredentials = result.withCredentials; + if (result.responseType != null) { + self.responseType = result.responseType; + }; + if (result.headers != null) { + for (var header in result.headers) { + var value = result.headers[header]; + var flutter_inappwebview_value = self._flutter_inappwebview_request_headers[header]; + if (flutter_inappwebview_value == null) { + self._flutter_inappwebview_request_headers[header] = value; + } else { + self._flutter_inappwebview_request_headers[header] += ', ' + value; + } + setRequestHeader.call(self, header, value); + }; + } + if ((self._flutter_inappwebview_method != result.method && result.method != null) || (self._flutter_inappwebview_url != result.url && result.url != null)) { + self.abort(); + self.open(result.method, result.url, result.isAsync, result.user, result.password); + return; + } + } + send.call(self, data); + }); + }); + } else { + send.call(this, data); + } + }; +})(window.XMLHttpRequest); +""" diff --git a/macos/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift b/macos/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift new file mode 100644 index 00000000..14539811 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift @@ -0,0 +1,153 @@ +// +// InterceptFetchRequestsJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useShouldInterceptFetchRequest" + +let INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT = PluginScript( + groupName: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: INTERCEPT_FETCH_REQUEST_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let INTERCEPT_FETCH_REQUEST_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) = true; +(function(fetch) { + if (fetch == null) { + return; + } + window.fetch = async function(resource, init) { + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) == true) { + var fetchRequest = { + url: null, + method: null, + headers: null, + body: null, + mode: null, + credentials: null, + cache: null, + redirect: null, + referrer: null, + referrerPolicy: null, + integrity: null, + keepalive: null + }; + if (resource instanceof Request) { + fetchRequest.url = resource.url; + fetchRequest.method = resource.method; + fetchRequest.headers = resource.headers; + fetchRequest.body = resource.body; + fetchRequest.mode = resource.mode; + fetchRequest.credentials = resource.credentials; + fetchRequest.cache = resource.cache; + fetchRequest.redirect = resource.redirect; + fetchRequest.referrer = resource.referrer; + fetchRequest.referrerPolicy = resource.referrerPolicy; + fetchRequest.integrity = resource.integrity; + fetchRequest.keepalive = resource.keepalive; + } else { + fetchRequest.url = resource != null ? resource.toString() : null; + if (init != null) { + fetchRequest.method = init.method; + fetchRequest.headers = init.headers; + fetchRequest.body = init.body; + fetchRequest.mode = init.mode; + fetchRequest.credentials = init.credentials; + fetchRequest.cache = init.cache; + fetchRequest.redirect = init.redirect; + fetchRequest.referrer = init.referrer; + fetchRequest.referrerPolicy = init.referrerPolicy; + fetchRequest.integrity = init.integrity; + fetchRequest.keepalive = init.keepalive; + } + } + if (fetchRequest.headers instanceof Headers) { + fetchRequest.headers = \(JAVASCRIPT_UTIL_VAR_NAME).convertHeadersToJson(fetchRequest.headers); + } + fetchRequest.credentials = \(JAVASCRIPT_UTIL_VAR_NAME).convertCredentialsToJson(fetchRequest.credentials); + return \(JAVASCRIPT_UTIL_VAR_NAME).convertBodyRequest(fetchRequest.body).then(function(body) { + fetchRequest.body = body; + return window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptFetchRequest', fetchRequest).then(function(result) { + if (result != null) { + switch (result.action) { + case 0: + var controller = new AbortController(); + if (init != null) { + init.signal = controller.signal; + } else { + init = { + signal: controller.signal + }; + } + controller.abort(); + break; + } + if (result.body != null && !\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.body) && result.body.length > 0) { + var bodyString = \(JAVASCRIPT_UTIL_VAR_NAME).arrayBufferToString(result.body); + if (\(JAVASCRIPT_UTIL_VAR_NAME).isBodyFormData(bodyString)) { + var formDataContentType = \(JAVASCRIPT_UTIL_VAR_NAME).getFormDataContentType(bodyString); + if (result.headers != null) { + result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type']; + } else { + result.headers = { 'Content-Type': formDataContentType }; + } + } + } + resource = result.url; + if (init == null) { + init = {}; + } + if (result.method != null && result.method.length > 0) { + init.method = result.method; + } + if (result.headers != null && Object.keys(result.headers).length > 0) { + init.headers = \(JAVASCRIPT_UTIL_VAR_NAME).convertJsonToHeaders(result.headers); + } + if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.body) || result.body == null) { + init.body = result.body; + } else if (result.body.length > 0) { + init.body = new Uint8Array(result.body); + } + if (result.mode != null && result.mode.length > 0) { + init.mode = result.mode; + } + if (result.credentials != null) { + init.credentials = \(JAVASCRIPT_UTIL_VAR_NAME).convertJsonToCredential(result.credentials); + } + if (result.cache != null && result.cache.length > 0) { + init.cache = result.cache; + } + if (result.redirect != null && result.redirect.length > 0) { + init.redirect = result.redirect; + } + if (result.referrer != null && result.referrer.length > 0) { + init.referrer = result.referrer; + } + if (result.referrerPolicy != null && result.referrerPolicy.length > 0) { + init.referrerPolicy = result.referrerPolicy; + } + if (result.integrity != null && result.integrity.length > 0) { + init.integrity = result.integrity; + } + if (result.keepalive != null) { + init.keepalive = result.keepalive; + } + return fetch(resource, init); + } + return fetch(resource, init); + }); + }); + } else { + return fetch(resource, init); + } + }; +})(window.fetch); +""" diff --git a/macos/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift b/macos/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift new file mode 100644 index 00000000..cddca764 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift @@ -0,0 +1,243 @@ +// +// javaScriptBridgeJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let JAVASCRIPT_BRIDGE_NAME = "flutter_inappwebview" +let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT" + +let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: JAVASCRIPT_BRIDGE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: ["callHandler"]) + +let JAVASCRIPT_BRIDGE_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME) = {}; +\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME) = {}; +window.\(JAVASCRIPT_BRIDGE_NAME).callHandler = function() { + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + var _callHandlerID = setTimeout(function(){}); + window.webkit.messageHandlers['callHandler'].postMessage( {'handlerName': arguments[0], '_callHandlerID': _callHandlerID, 'args': JSON.stringify(Array.prototype.slice.call(arguments, 1)), '_windowId': _windowId} ); + return new Promise(function(resolve, reject) { + window.\(JAVASCRIPT_BRIDGE_NAME)[_callHandlerID] = resolve; + }); +}; +\(WEB_MESSAGE_LISTENER_JS_SOURCE) +\(UTIL_JS_SOURCE) +""" + +let PLATFORM_READY_JS_SOURCE = "window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));"; + +let JAVASCRIPT_UTIL_VAR_NAME = "window.\(JAVASCRIPT_BRIDGE_NAME)._Util" + +/* + https://github.com/github/fetch/blob/master/fetch.js + */ +let UTIL_JS_SOURCE = """ +\(JAVASCRIPT_UTIL_VAR_NAME) = { + support: { + searchParams: 'URLSearchParams' in window, + iterable: 'Symbol' in window && 'iterator' in Symbol, + blob: + 'FileReader' in window && + 'Blob' in window && + (function() { + try { + new Blob(); + return true; + } catch (e) { + return false; + } + })(), + formData: 'FormData' in window, + arrayBuffer: 'ArrayBuffer' in window + }, + isDataView: function(obj) { + return obj && DataView.prototype.isPrototypeOf(obj); + }, + fileReaderReady: function(reader) { + return new Promise(function(resolve, reject) { + reader.onload = function() { + resolve(reader.result); + }; + reader.onerror = function() { + reject(reader.error); + }; + }); + }, + readBlobAsArrayBuffer: function(blob) { + var reader = new FileReader(); + var promise = \(JAVASCRIPT_UTIL_VAR_NAME).fileReaderReady(reader); + reader.readAsArrayBuffer(blob); + return promise; + }, + convertBodyToArrayBuffer: function(body) { + var viewClasses = [ + '[object Int8Array]', + '[object Uint8Array]', + '[object Uint8ClampedArray]', + '[object Int16Array]', + '[object Uint16Array]', + '[object Int32Array]', + '[object Uint32Array]', + '[object Float32Array]', + '[object Float64Array]' + ]; + var isArrayBufferView = null; + if (\(JAVASCRIPT_UTIL_VAR_NAME).support.arrayBuffer) { + isArrayBufferView = + ArrayBuffer.isView || + function(obj) { + return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1; + }; + } + + var bodyUsed = false; + + this._bodyInit = body; + if (!body) { + this._bodyText = ''; + } else if (typeof body === 'string') { + this._bodyText = body; + } else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body; + } else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body; + } else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this._bodyText = body.toString(); + } else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.arrayBuffer && \(JAVASCRIPT_UTIL_VAR_NAME).support.blob && \(JAVASCRIPT_UTIL_VAR_NAME).isDataView(body)) { + this._bodyArrayBuffer = bufferClone(body.buffer); + this._bodyInit = new Blob([this._bodyArrayBuffer]); + } else if (\(JAVASCRIPT_UTIL_VAR_NAME).support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { + this._bodyArrayBuffer = bufferClone(body); + } else { + this._bodyText = body = Object.prototype.toString.call(body); + } + + this.blob = function () { + if (bodyUsed) { + return Promise.reject(new TypeError('Already read')); + } + bodyUsed = true; + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob); + } else if (this._bodyArrayBuffer) { + return Promise.resolve(new Blob([this._bodyArrayBuffer])); + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob'); + } else { + return Promise.resolve(new Blob([this._bodyText])); + } + }; + + if (this._bodyArrayBuffer) { + if (bodyUsed) { + return Promise.reject(new TypeError('Already read')); + } + bodyUsed = true; + if (ArrayBuffer.isView(this._bodyArrayBuffer)) { + return Promise.resolve( + this._bodyArrayBuffer.buffer.slice( + this._bodyArrayBuffer.byteOffset, + this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength + ) + ); + } else { + return Promise.resolve(this._bodyArrayBuffer); + } + } + return this.blob().then(\(JAVASCRIPT_UTIL_VAR_NAME).readBlobAsArrayBuffer); + }, + isString: function(variable) { + return typeof variable === 'string' || variable instanceof String; + }, + convertBodyRequest: function(body) { + if (body == null) { + return new Promise((resolve, reject) => resolve(null)); + } + if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(body) || (\(JAVASCRIPT_UTIL_VAR_NAME).support.searchParams && body instanceof URLSearchParams)) { + return new Promise((resolve, reject) => resolve(body.toString())); + } + if (window.Response != null) { + return new Response(body).arrayBuffer().then(function(arrayBuffer) { + return Array.from(new Uint8Array(arrayBuffer)); + }); + } + return \(JAVASCRIPT_UTIL_VAR_NAME).convertBodyToArrayBuffer(body).then(function(arrayBuffer) { + return Array.from(new Uint8Array(arrayBuffer)); + }); + }, + arrayBufferToString: function(arrayBuffer) { + var uint8Array = new Uint8Array(arrayBuffer); + return uint8Array.reduce(function(acc, i) { return acc += String.fromCharCode.apply(null, [i]); }, ''); + }, + isBodyFormData: function(bodyString) { + return bodyString.indexOf('------WebKitFormBoundary') >= 0; + }, + getFormDataContentType: function(bodyString) { + var boundary = bodyString.substr(2, 40); + return 'multipart/form-data; boundary=' + boundary; + }, + convertHeadersToJson: function(headers) { + var headersObj = {}; + for (var header of headers.keys()) { + var value = headers.get(header); + headersObj[header] = value; + } + return headersObj; + }, + convertJsonToHeaders: function(headersJson) { + return new Headers(headersJson); + }, + convertCredentialsToJson: function(credentials) { + var credentialsObj = {}; + if (window.FederatedCredential != null && credentials instanceof FederatedCredential) { + credentialsObj.type = credentials.type; + credentialsObj.id = credentials.id; + credentialsObj.name = credentials.name; + credentialsObj.protocol = credentials.protocol; + credentialsObj.provider = credentials.provider; + credentialsObj.iconURL = credentials.iconURL; + } else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) { + credentialsObj.type = credentials.type; + credentialsObj.id = credentials.id; + credentialsObj.name = credentials.name; + credentialsObj.password = credentials.password; + credentialsObj.iconURL = credentials.iconURL; + } else { + credentialsObj.type = 'default'; + credentialsObj.value = credentials; + } + return credentialsObj; + }, + convertJsonToCredential: function(credentialsJson) { + var credentials; + if (window.FederatedCredential != null && credentialsJson.type === 'federated') { + credentials = new FederatedCredential({ + id: credentialsJson.id, + name: credentialsJson.name, + protocol: credentialsJson.protocol, + provider: credentialsJson.provider, + iconURL: credentialsJson.iconURL + }); + } else if (window.PasswordCredential != null && credentialsJson.type === 'password') { + credentials = new PasswordCredential({ + id: credentialsJson.id, + name: credentialsJson.name, + password: credentialsJson.password, + iconURL: credentialsJson.iconURL + }); + } else { + credentials = credentialsJson.value == null ? undefined : credentialsJson.value; + } + return credentials; + } +}; +""" diff --git a/macos/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift b/macos/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift new file mode 100644 index 00000000..ef2e7d67 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift @@ -0,0 +1,62 @@ +// +// LastTouchedAnchorOrImageJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT" + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = null; +window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = null; +(function() { + document.addEventListener('touchstart', function(event) { + var target = event.target; + while (target) { + if (target.tagName === 'IMG') { + var img = target; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = { + url: img.src + }; + var parent = img.parentNode; + while (parent) { + if (parent.tagName === 'A') { + window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = { + title: parent.textContent, + url: parent.href, + src: img.src + }; + break; + } + parent = parent.parentNode; + } + return; + } else if (target.tagName === 'A') { + var link = target; + var images = link.getElementsByTagName('img'); + var img = (images.length > 0) ? images[0] : null; + var imgSrc = (img != null) ? img.src : null; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = (img != null) ? {url: imgSrc} : window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = { + title: link.textContent, + url: link.href, + src: imgSrc + }; + return; + } + target = target.parentNode; + } + }); +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/OnLoadResourceJS.swift b/macos/Classes/PluginScriptsJS/OnLoadResourceJS.swift new file mode 100644 index 00000000..828de9ef --- /dev/null +++ b/macos/Classes/PluginScriptsJS/OnLoadResourceJS.swift @@ -0,0 +1,39 @@ +// +// resourceObserverJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useOnLoadResource" + +let ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_LOAD_RESOURCE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_LOAD_RESOURCE_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE) = true; +(function() { + var observer = new PerformanceObserver(function(list) { + list.getEntries().forEach(function(entry) { + if (\(FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE) == true) { + var resource = { + "url": entry.name, + "initiatorType": entry.initiatorType, + "startTime": entry.startTime, + "duration": entry.duration + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onLoadResource", resource); + } + }); + }); + observer.observe({entryTypes: ['resource']}); +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/OnScrollChangedJS.swift b/macos/Classes/PluginScriptsJS/OnScrollChangedJS.swift new file mode 100644 index 00000000..4418c042 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/OnScrollChangedJS.swift @@ -0,0 +1,33 @@ +// +// OnScrollEvent.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/10/22. +// + +import Foundation + +let ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT" + +let ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_SCROLL_CHANGED_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onScrollChanged"]) + +let ON_SCROLL_CHANGED_EVENT_JS_SOURCE = """ +(function(){ + document.addEventListener('scroll', function(e) { + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + window.webkit.messageHandlers["onScrollChanged"].postMessage( + { + x: window.scrollX, + y: window.scrollY, + _windowId: _windowId + } + ); + }); +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift b/macos/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift new file mode 100644 index 00000000..345b5ece --- /dev/null +++ b/macos/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift @@ -0,0 +1,26 @@ +// +// OnWindowBlurEventJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT" + +let ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_WINDOW_BLUR_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_WINDOW_BLUR_EVENT_JS_SOURCE = """ +(function(){ + window.addEventListener('blur', function(e) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowBlur'); + }); +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift b/macos/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift new file mode 100644 index 00000000..9ebda3e9 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift @@ -0,0 +1,26 @@ +// +// OnWindowFocusEventJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT" + +let ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + source: ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_WINDOW_FOCUS_EVENT_JS_SOURCE = """ +(function(){ + window.addEventListener('focus', function(e) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowFocus'); + }); +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift b/macos/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift new file mode 100644 index 00000000..a1c6637d --- /dev/null +++ b/macos/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift @@ -0,0 +1,31 @@ +// +// OriginalViewPortMetaTagContentJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT" + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_SOURCE, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = ""; +(function() { + var metaTagNodes = document.head.getElementsByTagName('meta'); + for (var i = 0; i < metaTagNodes.length; i++) { + var metaTagNode = metaTagNodes[i]; + if (metaTagNode.name === "viewport") { + window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = metaTagNode.content; + } + } +})(); +""" diff --git a/macos/Classes/PluginScriptsJS/PluginScriptsUtil.swift b/macos/Classes/PluginScriptsJS/PluginScriptsUtil.swift new file mode 100644 index 00000000..51f9c867 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/PluginScriptsUtil.swift @@ -0,0 +1,31 @@ +// +// PluginScripts.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public class PluginScriptsUtil { + public static let VAR_PLACEHOLDER_VALUE = "$IN_APP_WEBVIEW_PLACEHOLDER_VALUE" + public static let VAR_FUNCTION_ARGUMENT_NAMES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_NAMES" + public static let VAR_FUNCTION_ARGUMENT_VALUES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_VALUES" + public static let VAR_FUNCTION_ARGUMENTS_OBJ = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENTS_OBJ" + public static let VAR_FUNCTION_BODY = "$IN_APP_WEBVIEW_FUNCTION_BODY" + public static let VAR_RESULT_UUID = "$IN_APP_WEBVIEW_RESULT_UUID" + + public static let GET_SELECTED_TEXT_JS_SOURCE = """ +(function(){ + var txt; + if (window.getSelection) { + txt = window.getSelection().toString(); + } else if (window.document.getSelection) { + txt = window.document.getSelection().toString(); + } else if (window.document.selection) { + txt = window.document.selection.createRange().text; + } + return txt; +})(); +""" +} diff --git a/macos/Classes/PluginScriptsJS/PrintJS.swift b/macos/Classes/PluginScriptsJS/PrintJS.swift new file mode 100644 index 00000000..5ac12691 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/PrintJS.swift @@ -0,0 +1,24 @@ +// +// PrintJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PRINT_JS_PLUGIN_SCRIPT" + +let PRINT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: PRINT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let PRINT_JS_SOURCE = """ +window.print = function() { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onPrintRequest", window.location.href); +} +""" diff --git a/macos/Classes/PluginScriptsJS/PromisePolyfillJS.swift b/macos/Classes/PluginScriptsJS/PromisePolyfillJS.swift new file mode 100644 index 00000000..d2ef3e2a --- /dev/null +++ b/macos/Classes/PluginScriptsJS/PromisePolyfillJS.swift @@ -0,0 +1,27 @@ +// +// PromisePolyfillJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation +import WebKit + +let PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PROMISE_POLYFILL_JS_PLUGIN_SCRIPT" + +let PROMISE_POLYFILL_JS_PLUGIN_SCRIPT = PluginScript( + groupName: PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: PROMISE_POLYFILL_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +// https://github.com/tildeio/rsvp.js +let PROMISE_POLYFILL_JS_SOURCE = """ +if (window.Promise == null) { + !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.RSVP={})}(this,function(t){"use strict";function e(t){var e=t._promiseCallbacks;return e||(e=t._promiseCallbacks={}),e}var r={mixin:function(t){return t.on=this.on,t.off=this.off,t.trigger=this.trigger,t._promiseCallbacks=void 0,t},on:function(t,r){if("function"!=typeof r)throw new TypeError("Callback must be a function");var n=e(this),o=n[t];o||(o=n[t]=[]),-1===o.indexOf(r)&&o.push(r)},off:function(t,r){var n=e(this);if(r){var o=n[t],i=o.indexOf(r);-1!==i&&o.splice(i,1)}else n[t]=[]},trigger:function(t,r,n){var o=e(this)[t];if(o)for(var i=0;i2&&void 0!==arguments[2])||arguments[2],o=arguments[3];return function(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,e,r,n,o))}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype._init=function(t,e){this._result={},this._enumerate(e)},e.prototype._enumerate=function(t){var e=Object.keys(t),r=e.length,n=this.promise;this._remaining=r;for(var o=void 0,i=void 0,s=0;n._state===a&&s= 0) { + this.listeners.splice(index, 1); + } +}; + +window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6 = function(ip_string) { + // replace ipv4 address if any + var ipv4 = ip_string.match(/(.*:)([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$)/); + if (ipv4) { + ip_string = ipv4[1]; + ipv4 = ipv4[2].match(/[0-9]+/g); + for (var i = 0;i < 4;i ++) { + var byte = parseInt(ipv4[i],10); + ipv4[i] = ("0" + byte.toString(16)).substr(-2); + } + ip_string += ipv4[0] + ipv4[1] + ':' + ipv4[2] + ipv4[3]; + } + + // take care of leading and trailing :: + ip_string = ip_string.replace(/^:|:$/g, ''); + + var ipv6 = ip_string.split(':'); + + for (var i = 0; i < ipv6.length; i ++) { + var hex = ipv6[i]; + if (hex != "") { + // normalize leading zeros + ipv6[i] = ("0000" + hex).substr(-4); + } + else { + // normalize grouped zeros :: + hex = []; + for (var j = ipv6.length; j <= 8; j ++) { + hex.push('0000'); + } + ipv6[i] = hex.join(':'); + } + } + + return ipv6.join(':'); +} + +window.\(JAVASCRIPT_BRIDGE_NAME)._isOriginAllowed = function(allowedOriginRules, scheme, host, port) { + for (var rule of allowedOriginRules) { + if (rule === "*") { + return true; + } + if (scheme == null || scheme === "") { + continue; + } + if ((scheme == null || scheme === "") && (host == null || host === "") && (port === 0 || port === "" || port == null)) { + continue; + } + var rulePort = rule.port == null || rule.port === 0 ? (rule.scheme == "https" ? 443 : 80) : rule.port; + var currentPort = port === 0 || port === "" || port == null ? (scheme == "https" ? 443 : 80) : port; + var IPv6 = null; + if (rule.host != null && rule.host[0] === "[") { + try { + IPv6 = window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6(rule.host.substring(1, rule.host.length - 1)); + } catch {} + } + var hostIPv6 = null; + try { + hostIPv6 = window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6(host); + } catch {} + + var schemeAllowed = scheme == rule.scheme; + + var hostAllowed = rule.host == null || + rule.host === "" || + host === rule.host || + (rule.host[0] === "*" && host != null && host.indexOf(rule.host.split("*")[1]) >= 0) || + (hostIPv6 != null && IPv6 != null && hostIPv6 === IPv6); + + var portAllowed = rulePort === currentPort; + + if (schemeAllowed && hostAllowed && portAllowed) { + return true; + } + } + return false; +} +""" diff --git a/macos/Classes/PluginScriptsJS/WindowIdJS.swift b/macos/Classes/PluginScriptsJS/WindowIdJS.swift new file mode 100644 index 00000000..8b3a1879 --- /dev/null +++ b/macos/Classes/PluginScriptsJS/WindowIdJS.swift @@ -0,0 +1,19 @@ +// +// WindowIdJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let WINDOW_ID_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_WINDOW_ID_JS_PLUGIN_SCRIPT" + +let WINDOW_ID_VARIABLE_JS_SOURCE = "window._\(JAVASCRIPT_BRIDGE_NAME)_windowId" + +let WINDOW_ID_INITIALIZE_JS_SOURCE = """ +(function() { + \(WINDOW_ID_VARIABLE_JS_SOURCE) = \(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE); + return \(WINDOW_ID_VARIABLE_JS_SOURCE); +})() +""" diff --git a/macos/Classes/PrintJob/CustomUIPrintPageRenderer.swift b/macos/Classes/PrintJob/CustomUIPrintPageRenderer.swift new file mode 100644 index 00000000..c1976a15 --- /dev/null +++ b/macos/Classes/PrintJob/CustomUIPrintPageRenderer.swift @@ -0,0 +1,34 @@ +// +// CustomUIPrintPageRenderer.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation +// +//public class CustomUIPrintPageRenderer : UIPrintPageRenderer { +// private var _numberOfPages: Int? +// private var forceRenderingQuality: Int? +// +// public init(numberOfPage: Int? = nil, forceRenderingQuality: Int? = nil) { +// super.init() +// self._numberOfPages = numberOfPage +// self.forceRenderingQuality = forceRenderingQuality +// } +// +// open override var numberOfPages: Int { +// get { +// return _numberOfPages ?? super.numberOfPages +// } +// } +// +// @available(iOS 14.5, *) +// open override func currentRenderingQuality(forRequested requestedRenderingQuality: UIPrintRenderingQuality) -> UIPrintRenderingQuality { +// if let forceRenderingQuality = forceRenderingQuality, +// let quality = UIPrintRenderingQuality.init(rawValue: forceRenderingQuality) { +// return quality +// } +// return super.currentRenderingQuality(forRequested: requestedRenderingQuality) +// } +//} diff --git a/macos/Classes/PrintJob/PrintAttributes.swift b/macos/Classes/PrintJob/PrintAttributes.swift new file mode 100644 index 00000000..21178457 --- /dev/null +++ b/macos/Classes/PrintJob/PrintAttributes.swift @@ -0,0 +1,42 @@ +// +// PrintAttributes.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +public class PrintAttributes : NSObject { + var orientation: NSPrintInfo.PaperOrientation? + var margins: NSEdgeInsets? + var paperRect: CGRect? + var colorMode: String? + var duplex: Int? + + public init(fromPrintJobController: PrintJobController) { + super.init() + if let job = fromPrintJobController.job { + let printInfo = job.printInfo + orientation = printInfo.orientation + margins = NSEdgeInsets(top: printInfo.topMargin, + left: printInfo.leftMargin, + bottom: printInfo.bottomMargin, + right: printInfo.rightMargin) + paperRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: printInfo.paperSize) + colorMode = printInfo.printSettings["ColorModel"] as? String + duplex = printInfo.printSettings["com_apple_print_PrintSettings_PMDuplexing"] as? Int + print(printInfo.printSettings) + } + } + + public func toMap () -> [String:Any?] { + return [ + "paperRect": paperRect?.toMap(), + "margins": margins?.toMap(), + "orientation": orientation?.rawValue, + "colorMode": colorMode, + "duplex": duplex + ] + } +} diff --git a/macos/Classes/PrintJob/PrintJobChannelDelegate.swift b/macos/Classes/PrintJob/PrintJobChannelDelegate.swift new file mode 100644 index 00000000..4c8dcd48 --- /dev/null +++ b/macos/Classes/PrintJob/PrintJobChannelDelegate.swift @@ -0,0 +1,60 @@ +// +// PrintJobChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation +import FlutterMacOS + +public class PrintJobChannelDelegate : ChannelDelegate { + private weak var printJobController: PrintJobController? + + public init(printJobController: PrintJobController, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.printJobController = printJobController + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getInfo": + if let printJobController = printJobController { + result(printJobController.getInfo()?.toMap()) + } else { + result(false) + } + break + case "dispose": + if let printJobController = printJobController { + printJobController.dispose() + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onComplete(completed: Bool, error: Error?) { + let arguments: [String: Any?] = [ + "completed": completed, + "error": error?.localizedDescription + ] + channel?.invokeMethod("onComplete", arguments: arguments) + } + + public override func dispose() { + super.dispose() + printJobController = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/PrintJob/PrintJobController.swift b/macos/Classes/PrintJob/PrintJobController.swift new file mode 100644 index 00000000..30637c2b --- /dev/null +++ b/macos/Classes/PrintJob/PrintJobController.swift @@ -0,0 +1,88 @@ +// +// PrintJob.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation +import FlutterMacOS + +public enum PrintJobState: Int { + case created = 1 + case started = 3 + case completed = 5 + case failed = 6 + case canceled = 7 +} + +public class PrintJobController : NSObject, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_printjobcontroller_" + var id: String + var job: NSPrintOperation? + var settings: PrintJobSettings? + var channelDelegate: PrintJobChannelDelegate? + var state = PrintJobState.created + var creationTime = Int64(Date().timeIntervalSince1970 * 1000) + private var completionHandler: PrintJobController.CompletionHandler? + + public typealias CompletionHandler = (_ printOperation: NSPrintOperation, + _ success: Bool, + _ contextInfo: UnsafeMutableRawPointer?) -> Void + + public init(id: String, job: NSPrintOperation? = nil, settings: PrintJobSettings? = nil) { + self.id = id + super.init() + self.job = job + self.settings = settings + let channel = FlutterMethodChannel(name: PrintJobController.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + self.channelDelegate = PrintJobChannelDelegate(printJobController: self, channel: channel) + } + + public func present(parentWindow: NSWindow? = nil, completionHandler: PrintJobController.CompletionHandler? = nil) { + guard let job = job else { + return + } + state = .started + self.completionHandler = completionHandler + if let mainWindow = parentWindow ?? NSApplication.shared.mainWindow { + job.runModal(for: mainWindow, delegate: self, didRun: #selector(printOperationDidRun), contextInfo: nil) + } + } + + @objc func printOperationDidRun(printOperation: NSPrintOperation, + success: Bool, + contextInfo: UnsafeMutableRawPointer?) { + state = success ? .completed : .canceled + channelDelegate?.onComplete(completed: success, error: nil) + if let completionHandler = completionHandler { + completionHandler(printOperation, success, contextInfo) + self.completionHandler = nil + } + } + + public func getInfo() -> PrintJobInfo? { + guard let _ = job else { + return nil + } + + return PrintJobInfo.init(fromPrintJobController: self) + } + + public func disposeNoDismiss() { + channelDelegate?.dispose() + channelDelegate = nil + completionHandler = nil + job = nil + PrintJobManager.jobs[id] = nil + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + completionHandler = nil + job = nil + PrintJobManager.jobs[id] = nil + } +} diff --git a/macos/Classes/PrintJob/PrintJobInfo.swift b/macos/Classes/PrintJob/PrintJobInfo.swift new file mode 100644 index 00000000..6937c077 --- /dev/null +++ b/macos/Classes/PrintJob/PrintJobInfo.swift @@ -0,0 +1,53 @@ +// +// PrintJobInfo.swift +// flutter_downloader +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +public class PrintJobInfo : NSObject { + var state: PrintJobState + var attributes: PrintAttributes + var creationTime: Int64 + var numberOfPages: Int? + var copies: Int? + var label: String? + var printerName: String? + var printerType: String? + + public init(fromPrintJobController: PrintJobController) { + state = fromPrintJobController.state + creationTime = fromPrintJobController.creationTime + attributes = PrintAttributes.init(fromPrintJobController: fromPrintJobController) + if let job = fromPrintJobController.job { + let printInfo = job.printInfo + printerName = printInfo.printer.name + printerType = printInfo.printer.type.rawValue + copies = printInfo.printSettings["com_apple_print_PrintSettings_PMCopies"] as? Int + } + super.init() + if let job = fromPrintJobController.job { + let printInfo = job.printInfo + label = job.jobTitle + numberOfPages = printInfo.printSettings["com_apple_print_PrintSettings_PMLastPage"] as? Int + if numberOfPages == nil || numberOfPages! > job.pageRange.length { + numberOfPages = job.pageRange.length + } + } + } + + public func toMap () -> [String:Any?] { + return [ + "state": state.rawValue, + "attributes": attributes.toMap(), + "numberOfPages": numberOfPages, + "copies": copies, + "creationTime": creationTime, + "label": label, + "printerName": printerName, + "printerType": printerType + ] + } +} diff --git a/macos/Classes/PrintJob/PrintJobManager.swift b/macos/Classes/PrintJob/PrintJobManager.swift new file mode 100644 index 00000000..7148027a --- /dev/null +++ b/macos/Classes/PrintJob/PrintJobManager.swift @@ -0,0 +1,28 @@ +// +// PrintJobManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation + +public class PrintJobManager: NSObject, Disposable { + static var jobs: [String: PrintJobController?] = [:] + + public override init() { + super.init() + } + + public func dispose() { + let jobs = PrintJobManager.jobs.values + jobs.forEach { (job: PrintJobController?) in + job?.dispose() + } + PrintJobManager.jobs.removeAll() + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/PrintJob/PrintJobSettings.swift b/macos/Classes/PrintJob/PrintJobSettings.swift new file mode 100644 index 00000000..15abee8c --- /dev/null +++ b/macos/Classes/PrintJob/PrintJobSettings.swift @@ -0,0 +1,154 @@ +// +// PrintJobSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation + +@objcMembers +public class PrintJobSettings: ISettings { + + public var handledByClient = false + public var jobName: String? + public var animated = true + public var _orientation: NSNumber? + public var orientation: Int? { + get { + return _orientation?.intValue + } + set { + if let newValue = newValue { + _orientation = NSNumber.init(value: newValue) + } else { + _orientation = nil + } + } + } + public var _numberOfPages: NSNumber? + public var numberOfPages: Int? { + get { + return _numberOfPages?.intValue + } + set { + if let newValue = newValue { + _numberOfPages = NSNumber.init(value: newValue) + } else { + _numberOfPages = nil + } + } + } + public var _forceRenderingQuality: NSNumber? + public var forceRenderingQuality: Int? { + get { + return _forceRenderingQuality?.intValue + } + set { + if let newValue = newValue { + _forceRenderingQuality = NSNumber.init(value: newValue) + } else { + _forceRenderingQuality = nil + } + } + } + public var margins: NSEdgeInsets? + public var _duplexMode: NSNumber? + public var duplexMode: Int? { + get { + return _duplexMode?.intValue + } + set { + if let newValue = newValue { + _duplexMode = NSNumber.init(value: newValue) + } else { + _duplexMode = nil + } + } + } + public var _outputType: NSNumber? + public var outputType: Int? { + get { + return _outputType?.intValue + } + set { + if let newValue = newValue { + _outputType = NSNumber.init(value: newValue) + } else { + _outputType = nil + } + } + } + public var showsNumberOfCopies = true + public var showsPaperSelectionForLoadedPapers = false + public var showsPaperOrientation = true + public var _maximumContentHeight: NSNumber? + public var maximumContentHeight: Double? { + get { + return _maximumContentHeight?.doubleValue + } + set { + if let newValue = newValue { + _maximumContentHeight = NSNumber.init(value: newValue) + } else { + _maximumContentHeight = nil + } + } + } + public var _maximumContentWidth: NSNumber? + public var maximumContentWidth: Double? { + get { + return _maximumContentWidth?.doubleValue + } + set { + if let newValue = newValue { + _maximumContentWidth = NSNumber.init(value: newValue) + } else { + _maximumContentWidth = nil + } + } + } + public var _footerHeight: NSNumber? + public var footerHeight: Double? { + get { + return _footerHeight?.doubleValue + } + set { + if let newValue = newValue { + _footerHeight = NSNumber.init(value: newValue) + } else { + _footerHeight = nil + } + } + } + public var _headerHeight: NSNumber? + public var headerHeight: Double? { + get { + return _headerHeight?.doubleValue + } + set { + if let newValue = newValue { + _headerHeight = NSNumber.init(value: newValue) + } else { + _headerHeight = nil + } + } + } + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> PrintJobSettings { + let _ = super.parse(settings: settings) + if let marginsMap = settings["margins"] as? [String : Double] { + margins = NSEdgeInsets.fromMap(map: marginsMap) + } + return self + } + + override func getRealSettings(obj: PrintJobController?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + return realOptions + } +} diff --git a/macos/Classes/SwiftFlutterPlugin.swift b/macos/Classes/SwiftFlutterPlugin.swift new file mode 100755 index 00000000..3a60cbca --- /dev/null +++ b/macos/Classes/SwiftFlutterPlugin.swift @@ -0,0 +1,86 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation +import SafariServices + +public class SwiftFlutterPlugin: NSObject, FlutterPlugin { + + static var instance: SwiftFlutterPlugin? + var registrar: FlutterPluginRegistrar? + var platformUtil: PlatformUtil? + var inAppWebViewStatic: InAppWebViewStatic? + var myCookieManager: Any? + var myWebStorageManager: MyWebStorageManager? + var credentialDatabase: CredentialDatabase? + var inAppBrowserManager: InAppBrowserManager? + var headlessInAppWebViewManager: HeadlessInAppWebViewManager? + var webAuthenticationSessionManager: WebAuthenticationSessionManager? +// var printJobManager: PrintJobManager? + + var webViewControllers: [String: InAppBrowserWebViewController?] = [:] + var safariViewControllers: [String: Any?] = [:] + + public init(with registrar: FlutterPluginRegistrar) { + super.init() + self.registrar = registrar + registrar.register(FlutterWebViewFactory(registrar: registrar) as FlutterPlatformViewFactory, withId: FlutterWebViewFactory.VIEW_TYPE_ID) + + platformUtil = PlatformUtil(registrar: registrar) + inAppBrowserManager = InAppBrowserManager(registrar: registrar) + headlessInAppWebViewManager = HeadlessInAppWebViewManager(registrar: registrar) + inAppWebViewStatic = InAppWebViewStatic(registrar: registrar) + credentialDatabase = CredentialDatabase(registrar: registrar) + if #available(macOS 10.13, *) { + myCookieManager = MyCookieManager(registrar: registrar) + } + myWebStorageManager = MyWebStorageManager(registrar: registrar) + webAuthenticationSessionManager = WebAuthenticationSessionManager(registrar: registrar) +// printJobManager = PrintJobManager() + } + + public static func register(with registrar: FlutterPluginRegistrar) { + SwiftFlutterPlugin.instance = SwiftFlutterPlugin(with: registrar) + } + + public func detachFromEngine(for registrar: FlutterPluginRegistrar) { + platformUtil?.dispose() + platformUtil = nil + inAppBrowserManager?.dispose() + inAppBrowserManager = nil + headlessInAppWebViewManager?.dispose() + headlessInAppWebViewManager = nil + inAppWebViewStatic?.dispose() + inAppWebViewStatic = nil + credentialDatabase?.dispose() + credentialDatabase = nil + if #available(macOS 10.13, *) { + (myCookieManager as! MyCookieManager?)?.dispose() + myCookieManager = nil + } + myWebStorageManager?.dispose() + myWebStorageManager = nil + webAuthenticationSessionManager?.dispose() + webAuthenticationSessionManager = nil +// printJobManager?.dispose() +// printJobManager = nil + } +} diff --git a/macos/Classes/Types/BaseCallbackResult.swift b/macos/Classes/Types/BaseCallbackResult.swift new file mode 100644 index 00000000..f7edb4a5 --- /dev/null +++ b/macos/Classes/Types/BaseCallbackResult.swift @@ -0,0 +1,32 @@ +// +// BaseCallbackResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public class BaseCallbackResult : CallbackResult { + + override init() { + super.init() + + self.success = { [weak self] (obj: Any?) in + let result: T? = self?.decodeResult(obj) + var shouldRunDefaultBehaviour = false + if let result = result { + shouldRunDefaultBehaviour = self?.nonNullSuccess(result) ?? shouldRunDefaultBehaviour + } else { + shouldRunDefaultBehaviour = self?.nullSuccess() ?? shouldRunDefaultBehaviour + } + if shouldRunDefaultBehaviour { + self?.defaultBehaviour(result) + } + } + + self.notImplemented = { [weak self] in + self?.defaultBehaviour(nil) + } + } +} diff --git a/macos/Classes/Types/CGRect.swift b/macos/Classes/Types/CGRect.swift new file mode 100644 index 00000000..61cb7627 --- /dev/null +++ b/macos/Classes/Types/CGRect.swift @@ -0,0 +1,23 @@ +// +// CGRect.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +extension CGRect { + public static func fromMap(map: [String: Double]) -> CGRect { + return CGRect(x: map["x"]!, y: map["y"]!, width: map["width"]!, height: map["height"]!) + } + + public func toMap () -> [String:Any?] { + return [ + "x": minX, + "y": minY, + "width": width, + "height": height + ] + } +} diff --git a/macos/Classes/Types/CallbackResult.swift b/macos/Classes/Types/CallbackResult.swift new file mode 100644 index 00000000..5b0ba2b5 --- /dev/null +++ b/macos/Classes/Types/CallbackResult.swift @@ -0,0 +1,18 @@ +// +// CallbackResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public class CallbackResult : MethodChannelResult { + public var notImplemented: () -> Void = {} + public var success: (Any?) -> Void = {_ in } + public var error: (String, String?, Any?) -> Void = {_,_,_ in } + public var nonNullSuccess: (T) -> Bool = {_ in true} + public var nullSuccess: () -> Bool = {true} + public var defaultBehaviour: (T?) -> Void = {_ in } + public var decodeResult: (Any?) -> T? = {_ in nil} +} diff --git a/macos/Classes/Types/ChannelDelegate.swift b/macos/Classes/Types/ChannelDelegate.swift new file mode 100644 index 00000000..b8e7030f --- /dev/null +++ b/macos/Classes/Types/ChannelDelegate.swift @@ -0,0 +1,28 @@ +// +// ChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 04/05/22. +// + +import Foundation +import FlutterMacOS + +public class ChannelDelegate : FlutterMethodCallDelegate, Disposable { + var channel: FlutterMethodChannel? + + public init(channel: FlutterMethodChannel) { + super.init() + self.channel = channel + self.channel?.setMethodCallHandler(handle) + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + } + + public func dispose() { + channel?.setMethodCallHandler(nil) + channel = nil + } +} diff --git a/macos/Classes/Types/ClientCertChallenge.swift b/macos/Classes/Types/ClientCertChallenge.swift new file mode 100644 index 00000000..f2381ee4 --- /dev/null +++ b/macos/Classes/Types/ClientCertChallenge.swift @@ -0,0 +1,22 @@ +// +// ClientCertChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class ClientCertChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + ] + } +} diff --git a/macos/Classes/Types/ClientCertResponse.swift b/macos/Classes/Types/ClientCertResponse.swift new file mode 100644 index 00000000..75ae6818 --- /dev/null +++ b/macos/Classes/Types/ClientCertResponse.swift @@ -0,0 +1,33 @@ +// +// ClientCertResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class ClientCertResponse : NSObject { + var certificatePath: String + var certificatePassword: String? + var keyStoreType: String? + var action: Int? + + public init(certificatePath: String, certificatePassword: String? = nil, keyStoreType: String? = nil, action: Int? = nil) { + self.certificatePath = certificatePath + self.certificatePassword = certificatePassword + self.keyStoreType = keyStoreType + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> ClientCertResponse? { + guard let map = map else { + return nil + } + let certificatePath = map["certificatePath"] as! String + let certificatePassword = map["certificatePassword"] as? String + let keyStoreType = map["keyStoreType"] as? String + let action = map["action"] as? Int + return ClientCertResponse(certificatePath: certificatePath, certificatePassword: certificatePassword, keyStoreType: keyStoreType, action: action) + } +} diff --git a/macos/Classes/Types/CreateWindowAction.swift b/macos/Classes/Types/CreateWindowAction.swift new file mode 100644 index 00000000..db7f7658 --- /dev/null +++ b/macos/Classes/Types/CreateWindowAction.swift @@ -0,0 +1,31 @@ +// +// CreateWindowAction.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import WebKit + +public class CreateWindowAction : NSObject { + var navigationAction: WKNavigationAction + var windowId: Int64 + var windowFeatures: WKWindowFeatures + var isDialog: Bool? + + public init(navigationAction: WKNavigationAction, windowId: Int64, windowFeatures: WKWindowFeatures, isDialog: Bool? = nil) { + self.navigationAction = navigationAction + self.windowId = windowId + self.windowFeatures = windowFeatures + self.isDialog = isDialog + } + + public func toMap () -> [String:Any?] { + var map = navigationAction.toMap() + map["windowId"] = windowId + map["windowFeatures"] = windowFeatures.toMap() + map["isDialog"] = isDialog + return map + } +} diff --git a/macos/Classes/Types/CustomSchemeResponse.swift b/macos/Classes/Types/CustomSchemeResponse.swift new file mode 100644 index 00000000..ba5f0c60 --- /dev/null +++ b/macos/Classes/Types/CustomSchemeResponse.swift @@ -0,0 +1,31 @@ +// +// CustomSchemeResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import FlutterMacOS + +public class CustomSchemeResponse : NSObject { + var data: Data + var contentType: String + var contentEncoding: String + + public init(data: Data, contentType: String, contentEncoding: String) { + self.data = data + self.contentType = contentType + self.contentEncoding = contentEncoding + } + + public static func fromMap(map: [String:Any?]?) -> CustomSchemeResponse? { + guard let map = map else { + return nil + } + let data = map["data"] as! FlutterStandardTypedData + let contentType = map["contentType"] as! String + let contentEncoding = map["contentEncoding"] as! String + return CustomSchemeResponse(data: data.data, contentType: contentType, contentEncoding: contentEncoding) + } +} diff --git a/macos/Classes/Types/Disposable.swift b/macos/Classes/Types/Disposable.swift new file mode 100644 index 00000000..0a34a942 --- /dev/null +++ b/macos/Classes/Types/Disposable.swift @@ -0,0 +1,12 @@ +// +// Disposable.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 04/05/22. +// + +import Foundation + +public protocol Disposable { + func dispose() -> Void +} diff --git a/macos/Classes/Types/DownloadStartRequest.swift b/macos/Classes/Types/DownloadStartRequest.swift new file mode 100644 index 00000000..808f9453 --- /dev/null +++ b/macos/Classes/Types/DownloadStartRequest.swift @@ -0,0 +1,42 @@ +// +// DownloadStartRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/04/22. +// + +import Foundation + +public class DownloadStartRequest: NSObject { + var url: String + var userAgent: String? + var contentDisposition: String? + var mimeType: String? + var contentLength: Int64 + var suggestedFilename: String? + var textEncodingName: String? + + public init(url: String, userAgent: String?, contentDisposition: String?, + mimeType: String?, contentLength: Int64, + suggestedFilename: String?, textEncodingName: String?) { + self.url = url + self.userAgent = userAgent + self.contentDisposition = contentDisposition + self.mimeType = mimeType + self.contentLength = contentLength + self.suggestedFilename = suggestedFilename + self.textEncodingName = textEncodingName + } + + public func toMap () -> [String:Any?] { + return [ + "url": url, + "userAgent": userAgent, + "contentDisposition": contentDisposition, + "mimeType": mimeType, + "contentLength": contentLength, + "suggestedFilename": suggestedFilename, + "textEncodingName": textEncodingName + ] + } +} diff --git a/macos/Classes/Types/FindSession.swift b/macos/Classes/Types/FindSession.swift new file mode 100644 index 00000000..0c247004 --- /dev/null +++ b/macos/Classes/Types/FindSession.swift @@ -0,0 +1,28 @@ +// +// UIFindSession.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +public class FindSession: NSObject { + var resultCount: Int + var highlightedResultIndex: Int + var searchResultDisplayStyle: Int + + public init(resultCount: Int, highlightedResultIndex: Int, searchResultDisplayStyle: Int) { + self.resultCount = resultCount + self.highlightedResultIndex = highlightedResultIndex + self.searchResultDisplayStyle = searchResultDisplayStyle + } + + public func toMap () -> [String:Any?] { + return [ + "resultCount": resultCount, + "highlightedResultIndex": highlightedResultIndex, + "searchResultDisplayStyle": searchResultDisplayStyle + ] + } +} diff --git a/macos/Classes/Types/FlutterMethodCallDelegate.swift b/macos/Classes/Types/FlutterMethodCallDelegate.swift new file mode 100755 index 00000000..0454d2f7 --- /dev/null +++ b/macos/Classes/Types/FlutterMethodCallDelegate.swift @@ -0,0 +1,19 @@ +// +// FlutterMethodCallDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/12/2019. +// + +import Foundation +import FlutterMacOS + +public class FlutterMethodCallDelegate: NSObject { + public override init() { + super.init() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + } +} diff --git a/macos/Classes/Types/FlutterMethodChannel.swift b/macos/Classes/Types/FlutterMethodChannel.swift new file mode 100644 index 00000000..e9055b2e --- /dev/null +++ b/macos/Classes/Types/FlutterMethodChannel.swift @@ -0,0 +1,26 @@ +// +// FlutterMethodChannel.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation +import FlutterMacOS + +extension FlutterMethodChannel { + public func invokeMethod(_ method: String, arguments: Any, callback: MethodChannelResult) { + invokeMethod(method, arguments: arguments) {(result) -> Void in + if result is FlutterError { + let error = result as! FlutterError + callback.error(error.code, error.message, error.details) + } + else if (result as? NSObject) == FlutterMethodNotImplemented { + callback.notImplemented() + } + else { + callback.success(result) + } + } + } +} diff --git a/macos/Classes/Types/HitTestResult.swift b/macos/Classes/Types/HitTestResult.swift new file mode 100644 index 00000000..53307789 --- /dev/null +++ b/macos/Classes/Types/HitTestResult.swift @@ -0,0 +1,44 @@ +// +// HitTestResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public enum HitTestResultType: Int { + case unknownType = 0 + case phoneType = 2 + case geoType = 3 + case emailType = 4 + case imageType = 5 + case srcAnchorType = 7 + case srcImageAnchorType = 8 + case editTextType = 9 +} + +public class HitTestResult: NSObject { + var type: HitTestResultType + var extra: String? + + public init(type: HitTestResultType, extra: String?) { + self.type = type + self.extra = extra + } + + public static func fromMap(map: [String:Any?]?) -> HitTestResult? { + guard let map = map else { + return nil + } + let type = HitTestResultType.init(rawValue: map["type"] as? Int ?? HitTestResultType.unknownType.rawValue) ?? HitTestResultType.unknownType + return HitTestResult(type: type, extra: map["extra"] as? String) + } + + public func toMap () -> [String:Any?] { + return [ + "type": type.rawValue, + "extra": extra, + ] + } +} diff --git a/macos/Classes/Types/HttpAuthResponse.swift b/macos/Classes/Types/HttpAuthResponse.swift new file mode 100644 index 00000000..f2f85d64 --- /dev/null +++ b/macos/Classes/Types/HttpAuthResponse.swift @@ -0,0 +1,33 @@ +// +// HttpAuthResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class HttpAuthResponse : NSObject { + var username: String + var password: String + var permanentPersistence: Bool + var action: Int? + + public init(username: String, password: String, permanentPersistence: Bool, action: Int? = nil) { + self.username = username + self.password = password + self.permanentPersistence = permanentPersistence + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> HttpAuthResponse? { + guard let map = map else { + return nil + } + let username = map["username"] as! String + let password = map["password"] as! String + let permanentPersistence = map["permanentPersistence"] as! Bool + let action = map["action"] as? Int + return HttpAuthResponse(username: username, password: password, permanentPersistence: permanentPersistence, action: action) + } +} diff --git a/macos/Classes/Types/HttpAuthenticationChallenge.swift b/macos/Classes/Types/HttpAuthenticationChallenge.swift new file mode 100644 index 00000000..b22a0092 --- /dev/null +++ b/macos/Classes/Types/HttpAuthenticationChallenge.swift @@ -0,0 +1,34 @@ +// +// HttpAuthenticationChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class HttpAuthenticationChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + var previousFailureCount: Int = 0 + var failureResponse: URLResponse? + var error: Error? + var proposedCredential: URLCredential? + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + previousFailureCount = fromChallenge.previousFailureCount + failureResponse = fromChallenge.failureResponse + error = fromChallenge.error + proposedCredential = fromChallenge.proposedCredential + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + "previousFailureCount": previousFailureCount, + "failureResponse": failureResponse?.toMap(), + "error": error?.localizedDescription, + "proposedCredential": proposedCredential?.toMap() + ] + } +} diff --git a/macos/Classes/Types/JsAlertResponse.swift b/macos/Classes/Types/JsAlertResponse.swift new file mode 100644 index 00000000..3e6b3e78 --- /dev/null +++ b/macos/Classes/Types/JsAlertResponse.swift @@ -0,0 +1,33 @@ +// +// JsAlertResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public class JsAlertResponse: NSObject { + var message: String + var confirmButtonTitle: String + var handledByClient: Bool + var action: Int? + + public init(message: String, confirmButtonTitle: String, handledByClient: Bool, action: Int? = nil) { + self.message = message + self.confirmButtonTitle = confirmButtonTitle + self.handledByClient = handledByClient + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsAlertResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let action = map["action"] as? Int + return JsAlertResponse(message: message, confirmButtonTitle: confirmButtonTitle, handledByClient: handledByClient, action: action) + } +} diff --git a/macos/Classes/Types/JsConfirmResponse.swift b/macos/Classes/Types/JsConfirmResponse.swift new file mode 100644 index 00000000..76bfbe20 --- /dev/null +++ b/macos/Classes/Types/JsConfirmResponse.swift @@ -0,0 +1,36 @@ +// +// JsConfirmResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class JsConfirmResponse: NSObject { + var message: String + var confirmButtonTitle: String + var cancelButtonTitle: String + var handledByClient: Bool + var action: Int? + + public init(message: String, confirmButtonTitle: String, cancelButtonTitle: String, handledByClient: Bool, action: Int? = nil) { + self.message = message + self.confirmButtonTitle = confirmButtonTitle + self.cancelButtonTitle = cancelButtonTitle + self.handledByClient = handledByClient + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsConfirmResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let cancelButtonTitle = map["cancelButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let action = map["action"] as? Int + return JsConfirmResponse(message: message, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, handledByClient: handledByClient, action: action) + } +} diff --git a/macos/Classes/Types/JsPromptResponse.swift b/macos/Classes/Types/JsPromptResponse.swift new file mode 100644 index 00000000..5c40b492 --- /dev/null +++ b/macos/Classes/Types/JsPromptResponse.swift @@ -0,0 +1,43 @@ +// +// JsPromptResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class JsPromptResponse: NSObject { + var message: String + var defaultValue: String + var confirmButtonTitle: String + var cancelButtonTitle: String + var handledByClient: Bool + var value: String? + var action: Int? + + public init(message: String, defaultValue: String, confirmButtonTitle: String, cancelButtonTitle: String, handledByClient: Bool, value: String? = nil, action: Int? = nil) { + self.message = message + self.defaultValue = defaultValue + self.confirmButtonTitle = confirmButtonTitle + self.cancelButtonTitle = cancelButtonTitle + self.handledByClient = handledByClient + self.value = value + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsPromptResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let defaultValue = map["defaultValue"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let cancelButtonTitle = map["cancelButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let value = map["value"] as? String + let action = map["action"] as? Int + return JsPromptResponse(message: message, defaultValue: defaultValue, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, + handledByClient: handledByClient, value: value, action: action) + } +} diff --git a/macos/Classes/Types/MethodChannelResult.swift b/macos/Classes/Types/MethodChannelResult.swift new file mode 100644 index 00000000..d1555e39 --- /dev/null +++ b/macos/Classes/Types/MethodChannelResult.swift @@ -0,0 +1,14 @@ +// +// MethodChannelResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public protocol MethodChannelResult { + var success: (_ obj: Any?) -> Void { get set } + var error: (_ code: String, _ message: String?, _ details: Any?) -> Void { get set } + var notImplemented: () -> Void { get set } +} diff --git a/macos/Classes/Types/NSAttributedString.swift b/macos/Classes/Types/NSAttributedString.swift new file mode 100644 index 00000000..64380da6 --- /dev/null +++ b/macos/Classes/Types/NSAttributedString.swift @@ -0,0 +1,63 @@ +// +// NSAttributedString.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/03/21. +// + +import Foundation + +extension NSAttributedString { + public static func fromMap(map: [String:Any?]?) -> NSAttributedString? { + guard let map = map, let string = map["string"] as? String else { + return nil + } + + var attributes: [NSAttributedString.Key : Any] = [:] + + if let backgroundColor = map["backgroundColor"] as? String { + attributes[.backgroundColor] = NSColor(hexString: backgroundColor) + } + if let baselineOffset = map["baselineOffset"] as? Double { + attributes[.baselineOffset] = baselineOffset + } + if let expansion = map["expansion"] as? Double { + attributes[.expansion] = expansion + } + if let foregroundColor = map["foregroundColor"] as? String { + attributes[.foregroundColor] = NSColor(hexString: foregroundColor) + } + if let kern = map["kern"] as? Double { + attributes[.kern] = kern + } + if let ligature = map["ligature"] as? Int64 { + attributes[.ligature] = ligature + } + if let obliqueness = map["obliqueness"] as? Double { + attributes[.obliqueness] = obliqueness + } + if let strikethroughColor = map["strikethroughColor"] as? String { + attributes[.strikethroughColor] = NSColor(hexString: strikethroughColor) + } + if let strikethroughStyle = map["strikethroughStyle"] as? Int64 { + attributes[.strikethroughStyle] = strikethroughStyle + } + if let strokeColor = map["strokeColor"] as? String { + attributes[.strokeColor] = NSColor(hexString: strokeColor) + } + if let strokeWidth = map["strokeWidth"] as? Double { + attributes[.strokeWidth] = strokeWidth + } + if let textEffect = map["textEffect"] as? String { + attributes[.textEffect] = textEffect + } + if let underlineColor = map["underlineColor"] as? String { + attributes[.underlineColor] = NSColor(hexString: underlineColor) + } + if let underlineStyle = map["underlineStyle"] as? Int64 { + attributes[.underlineStyle] = underlineStyle + } + + return NSAttributedString(string: string, attributes: attributes) + } +} diff --git a/macos/Classes/Types/NSColor.swift b/macos/Classes/Types/NSColor.swift new file mode 100644 index 00000000..263a5c24 --- /dev/null +++ b/macos/Classes/Types/NSColor.swift @@ -0,0 +1,57 @@ +// +// NSColor.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension NSColor { + convenience init(hexString: String) { + let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int = UInt64() + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (255, 0, 0, 0) + } + self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) + } + + var hexString: String? { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + let multiplier = CGFloat(255.999999) + + self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + if alpha == 1.0 { + return String( + format: "#%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier) + ) + } + else { + return String( + format: "#%02lX%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier), + Int(alpha * multiplier) + ) + } + } +} diff --git a/macos/Classes/Types/NSEdgeInsets.swift b/macos/Classes/Types/NSEdgeInsets.swift new file mode 100644 index 00000000..68aadcd2 --- /dev/null +++ b/macos/Classes/Types/NSEdgeInsets.swift @@ -0,0 +1,23 @@ +// +// UIEdgeInsets.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 11/05/22. +// + +import Foundation + +extension NSEdgeInsets { + public static func fromMap(map: [String: Double]) -> NSEdgeInsets { + return NSEdgeInsets.init(top: map["top"]!, left: map["left"]!, bottom: map["bottom"]!, right: map["right"]!) + } + + public func toMap () -> [String:Any?] { + return [ + "top": top, + "right": self.right, + "bottom": bottom, + "left": self.left + ] + } +} diff --git a/macos/Classes/Types/PermissionRequest.swift b/macos/Classes/Types/PermissionRequest.swift new file mode 100644 index 00000000..a286a3f3 --- /dev/null +++ b/macos/Classes/Types/PermissionRequest.swift @@ -0,0 +1,29 @@ +// +// PermissionRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 21/04/22. +// + +import Foundation +import WebKit + +public class PermissionRequest: NSObject { + var origin: String + var resources: [StringOrInt] + var frame: WKFrameInfo + + public init(origin: String, resources: [StringOrInt], frame: WKFrameInfo) { + self.origin = origin + self.resources = resources + self.frame = frame + } + + public func toMap () -> [String:Any?] { + return [ + "origin": origin, + "resources": resources, + "frame": frame.toMap() + ] + } +} diff --git a/macos/Classes/Types/PermissionResponse.swift b/macos/Classes/Types/PermissionResponse.swift new file mode 100644 index 00000000..7971f95e --- /dev/null +++ b/macos/Classes/Types/PermissionResponse.swift @@ -0,0 +1,27 @@ +// +// PermissionResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class PermissionResponse : NSObject { + var resources: [Any] + var action: Int? + + public init(resources: [Any], action: Int? = nil) { + self.resources = resources + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> PermissionResponse? { + guard let map = map else { + return nil + } + let resources = map["resources"] as! [Any] + let action = map["action"] as? Int + return PermissionResponse(resources: resources, action: action) + } +} diff --git a/macos/Classes/Types/PluginScript.swift b/macos/Classes/Types/PluginScript.swift new file mode 100644 index 00000000..503e440b --- /dev/null +++ b/macos/Classes/Types/PluginScript.swift @@ -0,0 +1,90 @@ +// +// PluginScript.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/02/21. +// + +import Foundation +import WebKit + +public class PluginScript : UserScript { + var requiredInAllContentWorlds = false + var messageHandlerNames: [String] = [] + + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + } + + public init(groupName: String, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(groupName: groupName, source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + @available(macOS 11.0, *) + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.contentWorld = contentWorld + } + + @available(macOS 11.0, *) + public init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + @available(macOS 11.0, *) + public init(groupName: String, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(groupName: groupName, source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + public func copyAndSet(groupName: String? = nil, + source: String? = nil, + injectionTime: WKUserScriptInjectionTime? = nil, + forMainFrameOnly: Bool? = nil, + requiredInAllContentWorlds: Bool? = nil, + messageHandlerNames: [String]? = nil) -> PluginScript { + if #available(macOS 11.0, *) { + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + in: self.contentWorld, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + + @available(macOS 11.0, *) + public func copyAndSet(groupName: String? = nil, + source: String? = nil, + injectionTime: WKUserScriptInjectionTime? = nil, + forMainFrameOnly: Bool? = nil, + contentWorld: WKContentWorld? = nil, + requiredInAllContentWorlds: Bool? = nil, + messageHandlerNames: [String]? = nil) -> PluginScript { + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + in: contentWorld ?? self.contentWorld, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } +} diff --git a/macos/Classes/Types/SecCertificate.swift b/macos/Classes/Types/SecCertificate.swift new file mode 100644 index 00000000..06a17970 --- /dev/null +++ b/macos/Classes/Types/SecCertificate.swift @@ -0,0 +1,18 @@ +// +// SecCertificate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension SecCertificate { + var data: Data { + let serverCertificateCFData = SecCertificateCopyData(self) + let data = CFDataGetBytePtr(serverCertificateCFData) + let size = CFDataGetLength(serverCertificateCFData) + let certificateData = NSData(bytes: data, length: size) + return Data(certificateData) + } +} diff --git a/macos/Classes/Types/ServerTrustAuthResponse.swift b/macos/Classes/Types/ServerTrustAuthResponse.swift new file mode 100644 index 00000000..d78fb07c --- /dev/null +++ b/macos/Classes/Types/ServerTrustAuthResponse.swift @@ -0,0 +1,24 @@ +// +// ServerTrustAuthResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class ServerTrustAuthResponse : NSObject { + var action: Int? + + public init(action: Int? = nil) { + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> ServerTrustAuthResponse? { + guard let map = map else { + return nil + } + let action = map["action"] as? Int + return ServerTrustAuthResponse(action: action) + } +} diff --git a/macos/Classes/Types/ServerTrustChallenge.swift b/macos/Classes/Types/ServerTrustChallenge.swift new file mode 100644 index 00000000..190c831b --- /dev/null +++ b/macos/Classes/Types/ServerTrustChallenge.swift @@ -0,0 +1,22 @@ +// +// ServerTrustChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class ServerTrustChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap() + ] + } +} diff --git a/macos/Classes/Types/Size2D.swift b/macos/Classes/Types/Size2D.swift new file mode 100644 index 00000000..7c2b9c6c --- /dev/null +++ b/macos/Classes/Types/Size2D.swift @@ -0,0 +1,35 @@ +// +// Size.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/03/21. +// + +import Foundation + +public class Size2D : NSObject { + var width: Double + var height: Double + + public init(width: Double, height: Double) { + self.width = width + self.height = height + } + + public static func fromMap(map: [String:Any?]?) -> Size2D? { + guard let map = map else { + return nil + } + return Size2D( + width: map["width"] as? Double ?? -1.0, + height: map["height"] as? Double ?? -1.0 + ) + } + + public func toMap() -> [String:Any?] { + return [ + "width": width, + "height": height + ] + } +} diff --git a/macos/Classes/Types/SslCertificate.swift b/macos/Classes/Types/SslCertificate.swift new file mode 100644 index 00000000..6b9d9a17 --- /dev/null +++ b/macos/Classes/Types/SslCertificate.swift @@ -0,0 +1,30 @@ +// +// SslCertificate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class SslCertificate: NSObject { + var x509Certificate: Data + var issuedBy: Any? + var issuedTo: Any? + var validNotAfterDate: Any? + var validNotBeforeDate: Any? + + public init(x509Certificate: Data) { + self.x509Certificate = x509Certificate + } + + public func toMap () -> [String:Any?] { + return [ + "x509Certificate": x509Certificate, + "issuedBy": issuedBy, + "issuedTo": issuedTo, + "validNotAfterDate": validNotAfterDate, + "validNotBeforeDate": validNotBeforeDate + ] + } +} diff --git a/macos/Classes/Types/SslError.swift b/macos/Classes/Types/SslError.swift new file mode 100644 index 00000000..58e3cf94 --- /dev/null +++ b/macos/Classes/Types/SslError.swift @@ -0,0 +1,50 @@ +// +// SslError.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class SslError: NSObject { + var errorType: SecTrustResultType? + var message: String? + + public init(errorType: SecTrustResultType?) { + self.errorType = errorType + + var sslErrorMessage: String? = nil + switch errorType { + case .deny: + sslErrorMessage = "Indicates a user-configured deny; do not proceed." + break + case .fatalTrustFailure: + sslErrorMessage = "Indicates a trust failure which cannot be overridden by the user." + break + case .invalid: + sslErrorMessage = "Indicates an invalid setting or result." + break + case .otherError: + sslErrorMessage = "Indicates a failure other than that of trust evaluation." + break + case .recoverableTrustFailure: + sslErrorMessage = "Indicates a trust policy failure which can be overridden by the user." + break + case .unspecified: + sslErrorMessage = "Indicates the evaluation succeeded and the certificate is implicitly trusted, but user intent was not explicitly specified." + break + default: + sslErrorMessage = nil + } + + self.message = sslErrorMessage + } + + public func toMap () -> [String:Any?] { + return [ + "code": errorType?.rawValue, + "message": message + ] + } +} diff --git a/macos/Classes/Types/StringOrInt.swift b/macos/Classes/Types/StringOrInt.swift new file mode 100644 index 00000000..f7cd40c7 --- /dev/null +++ b/macos/Classes/Types/StringOrInt.swift @@ -0,0 +1,13 @@ +// +// StringOrInt.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 21/04/22. +// + +import Foundation + +public protocol StringOrInt { } + +extension Int: StringOrInt { } +extension String: StringOrInt { } diff --git a/macos/Classes/Types/URLAuthenticationChallenge.swift b/macos/Classes/Types/URLAuthenticationChallenge.swift new file mode 100644 index 00000000..39b21fce --- /dev/null +++ b/macos/Classes/Types/URLAuthenticationChallenge.swift @@ -0,0 +1,20 @@ +// +// URLAuthenticationChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLAuthenticationChallenge { + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + "previousFailureCount": previousFailureCount, + "failureResponse": failureResponse?.toMap(), + "error": error?.localizedDescription, + "proposedCredential": proposedCredential?.toMap(), + ] + } +} diff --git a/macos/Classes/Types/URLCredential.swift b/macos/Classes/Types/URLCredential.swift new file mode 100644 index 00000000..469f1e15 --- /dev/null +++ b/macos/Classes/Types/URLCredential.swift @@ -0,0 +1,26 @@ +// +// URLCredential.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLCredential { + public func toMap () -> [String:Any?] { + var x509Certificates: [Data] = [] + // certificates could be nil!!! + if certificates != nil { + for certificate in certificates { + x509Certificates.append((certificate as! SecCertificate).data) + } + } + return [ + "password": password, + "username": user, + "certificates": x509Certificates, + "persistence": persistence.rawValue + ] + } +} diff --git a/macos/Classes/Types/URLProtectionSpace.swift b/macos/Classes/Types/URLProtectionSpace.swift new file mode 100644 index 00000000..75557e17 --- /dev/null +++ b/macos/Classes/Types/URLProtectionSpace.swift @@ -0,0 +1,63 @@ +// +// URLProtectionSpace.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLProtectionSpace { + + var x509Certificate: Data? { + guard let serverTrust = serverTrust else { + return nil + } + + var secResult = SecTrustResultType.invalid + let secTrustEvaluateStatus = SecTrustEvaluate(serverTrust, &secResult); + + if secTrustEvaluateStatus == errSecSuccess, let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { + return serverCertificate.data + } + return nil + } + + var sslCertificate: SslCertificate? { + var sslCertificate: SslCertificate? = nil + if let x509Certificate = x509Certificate { + sslCertificate = SslCertificate(x509Certificate: x509Certificate) + } + return sslCertificate + } + + var sslError: SslError? { + guard let serverTrust = serverTrust else { + return nil + } + + var secResult = SecTrustResultType.invalid + SecTrustEvaluate(serverTrust, &secResult); + + guard let sslErrorType = secResult != SecTrustResultType.proceed ? secResult : nil else { + return nil + } + + return SslError(errorType: sslErrorType) + } + + public func toMap () -> [String:Any?] { + return [ + "host": host, + "procotol": self.protocol, + "realm": realm, + "port": port, + "sslCertificate": sslCertificate?.toMap(), + "sslError": sslError?.toMap(), + "authenticationMethod": authenticationMethod, + "distinguishedNames": distinguishedNames, + "receivesCredentialSecurely": receivesCredentialSecurely, + "proxyType": proxyType + ] + } +} diff --git a/macos/Classes/Types/URLRequest.swift b/macos/Classes/Types/URLRequest.swift new file mode 100644 index 00000000..54a251d2 --- /dev/null +++ b/macos/Classes/Types/URLRequest.swift @@ -0,0 +1,99 @@ +// +// URLRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import FlutterMacOS + +extension URLRequest { + public init(fromPluginMap: [String:Any?]) { + if let urlString = fromPluginMap["url"] as? String, let url = URL(string: urlString) { + self.init(url: url) + } else { + self.init(url: URL(string: "about:blank")!) + } + + if let method = fromPluginMap["method"] as? String { + httpMethod = method + } + if let body = fromPluginMap["body"] as? FlutterStandardTypedData { + httpBody = body.data + } + if let headers = fromPluginMap["headers"] as? [String:String] { + for (key, value) in headers { + setValue(value, forHTTPHeaderField: key) + } + } + if let _allowsCellularAccess = fromPluginMap["allowsCellularAccess"] as? Bool { + allowsCellularAccess = _allowsCellularAccess + } + if #available(macOS 10.15, *), let _allowsConstrainedNetworkAccess = fromPluginMap["allowsConstrainedNetworkAccess"] as? Bool { + allowsConstrainedNetworkAccess = _allowsConstrainedNetworkAccess + } + if #available(macOS 10.15, *), let _allowsExpensiveNetworkAccess = fromPluginMap["allowsExpensiveNetworkAccess"] as? Bool { + allowsExpensiveNetworkAccess = _allowsExpensiveNetworkAccess + } + if let _cachePolicy = fromPluginMap["cachePolicy"] as? Int { + cachePolicy = CachePolicy.init(rawValue: UInt(_cachePolicy)) ?? .useProtocolCachePolicy + } + if let _httpShouldHandleCookies = fromPluginMap["httpShouldHandleCookies"] as? Bool { + httpShouldHandleCookies = _httpShouldHandleCookies + } + if let _httpShouldUsePipelining = fromPluginMap["httpShouldUsePipelining"] as? Bool { + httpShouldUsePipelining = _httpShouldUsePipelining + } + if let _networkServiceType = fromPluginMap["networkServiceType"] as? Int { + networkServiceType = NetworkServiceType.init(rawValue: UInt(_networkServiceType)) ?? .default + } + if let _timeoutInterval = fromPluginMap["timeoutInterval"] as? Double { + timeoutInterval = _timeoutInterval + } + if let _mainDocumentURL = fromPluginMap["mainDocumentURL"] as? String { + mainDocumentURL = URL(string: _mainDocumentURL)! + } + if #available(macOS 11.3, *), let _assumesHTTP3Capable = fromPluginMap["assumesHTTP3Capable"] as? Bool { + assumesHTTP3Capable = _assumesHTTP3Capable + } + if #available(macOS 12.0, *), let attributionRawValue = fromPluginMap["attribution"] as? UInt, + let _attribution = URLRequest.Attribution(rawValue: attributionRawValue) { + attribution = _attribution + } + } + + public func toMap () -> [String:Any?] { + var _allowsConstrainedNetworkAccess: Bool? = nil + var _allowsExpensiveNetworkAccess: Bool? = nil + if #available(macOS 10.15, *) { + _allowsConstrainedNetworkAccess = allowsConstrainedNetworkAccess + _allowsExpensiveNetworkAccess = allowsExpensiveNetworkAccess + } + var _assumesHTTP3Capable: Bool? = nil + if #available(macOS 11.3, *) { + _assumesHTTP3Capable = assumesHTTP3Capable + } + var _attribution: UInt? = nil + if #available(macOS 12.0, *) { + _attribution = attribution.rawValue + } + return [ + "url": url?.absoluteString, + "method": httpMethod, + "headers": allHTTPHeaderFields, + "body": httpBody.map(FlutterStandardTypedData.init(bytes:)), + "allowsCellularAccess": allowsCellularAccess, + "allowsConstrainedNetworkAccess": _allowsConstrainedNetworkAccess, + "allowsExpensiveNetworkAccess": _allowsExpensiveNetworkAccess, + "cachePolicy": cachePolicy.rawValue, + "httpShouldHandleCookies": httpShouldHandleCookies, + "httpShouldUsePipelining": httpShouldUsePipelining, + "networkServiceType": networkServiceType.rawValue, + "timeoutInterval": timeoutInterval, + "mainDocumentURL": mainDocumentURL?.absoluteString, + "assumesHTTP3Capable": _assumesHTTP3Capable, + "attribution": _attribution + ] + } +} diff --git a/macos/Classes/Types/URLResponse.swift b/macos/Classes/Types/URLResponse.swift new file mode 100644 index 00000000..0a6a4835 --- /dev/null +++ b/macos/Classes/Types/URLResponse.swift @@ -0,0 +1,31 @@ +// +// URLResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLResponse { + public convenience init?(fromPluginMap: [String:Any?]) { + let url = URL(string: fromPluginMap["url"] as? String ?? "about:blank")! + let mimeType = fromPluginMap["mimeType"] as? String + let expectedContentLength = fromPluginMap["expectedContentLength"] as? Int64 ?? 0 + let textEncodingName = fromPluginMap["textEncodingName"] as? String + self.init(url: url, mimeType: mimeType, expectedContentLength: Int(expectedContentLength), textEncodingName: textEncodingName) + } + + public func toMap () -> [String:Any?] { + let httpResponse: HTTPURLResponse? = self as? HTTPURLResponse + return [ + "expectedContentLength": expectedContentLength, + "mimeType": mimeType, + "suggestedFilename": suggestedFilename, + "textEncodingName": textEncodingName, + "url": url?.absoluteString, + "headers": httpResponse?.allHeaderFields, + "statusCode": httpResponse?.statusCode + ] + } +} diff --git a/macos/Classes/Types/UserScript.swift b/macos/Classes/Types/UserScript.swift new file mode 100644 index 00000000..63f30576 --- /dev/null +++ b/macos/Classes/Types/UserScript.swift @@ -0,0 +1,71 @@ +// +// InAppWebViewUserScript.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation +import WebKit + +public class UserScript : WKUserScript { + var groupName: String? + + private var contentWorldWrapper: Any? + @available(macOS 11.0, *) + var contentWorld: WKContentWorld { + get { + if let value = contentWorldWrapper as? WKContentWorld { + return value + } + return .page + } + set { contentWorldWrapper = newValue } + } + + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + } + + public init(groupName: String?, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + self.groupName = groupName + } + + @available(macOS 11.0, *) + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.contentWorld = contentWorld + } + + @available(macOS 11.0, *) + public init(groupName: String?, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.groupName = groupName + self.contentWorld = contentWorld + } + + public static func fromMap(map: [String:Any?]?, windowId: Int64?) -> UserScript? { + guard let map = map else { + return nil + } + + let contentWorldMap = map["contentWorld"] as? [String:Any?] + if #available(macOS 11.0, *), let contentWorldMap = contentWorldMap { + let contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: windowId)! + return UserScript( + groupName: map["groupName"] as? String, + source: map["source"] as! String, + injectionTime: WKUserScriptInjectionTime.init(rawValue: map["injectionTime"] as! Int) ?? .atDocumentStart, + forMainFrameOnly: map["forMainFrameOnly"] as! Bool, + in: contentWorld + ) + } + return UserScript( + groupName: map["groupName"] as? String, + source: map["source"] as! String, + injectionTime: WKUserScriptInjectionTime.init(rawValue: map["injectionTime"] as! Int) ?? .atDocumentStart, + forMainFrameOnly: map["forMainFrameOnly"] as! Bool + ) + } +} diff --git a/macos/Classes/Types/WKContentWorld.swift b/macos/Classes/Types/WKContentWorld.swift new file mode 100644 index 00000000..f94cc81a --- /dev/null +++ b/macos/Classes/Types/WKContentWorld.swift @@ -0,0 +1,41 @@ +// +// WKContentWorld.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +@available(macOS 11.0, *) +extension WKContentWorld { + // Workaround to create stored properties in an extension: + // https://valv0.medium.com/computed-properties-and-extensions-a-pure-swift-approach-64733768112c + + private static var _windowId = [String: Int64?]() + + var windowId: Int64? { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKContentWorld._windowId[tmpAddress] ?? nil + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKContentWorld._windowId[tmpAddress] = newValue + } + } + + public static func fromMap(map: [String:Any?]?, windowId: Int64?) -> WKContentWorld? { + guard let map = map else { + return nil + } + var name = map["name"] as! String + name = windowId != nil && name != "page" ? + WKUserContentController.WINDOW_ID_PREFIX + String(windowId!) + "-" + name : + name + let contentWorld = Util.getContentWorld(name: name) + contentWorld.windowId = name != "page" ? windowId : nil + return contentWorld + } +} diff --git a/macos/Classes/Types/WKFrameInfo.swift b/macos/Classes/Types/WKFrameInfo.swift new file mode 100644 index 00000000..042fe419 --- /dev/null +++ b/macos/Classes/Types/WKFrameInfo.swift @@ -0,0 +1,26 @@ +// +// WKFrameInfo.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKFrameInfo { + + public func toMap () -> [String:Any?] { + var securityOrigin: [String:Any?]? = nil + if #available(iOS 9.0, *) { + securityOrigin = self.securityOrigin.toMap() + } + // fix: self.request throws EXC_BREAKPOINT when coming from WKNavigationAction.sourceFrame + let request: URLRequest? = self.value(forKey: "request") as? URLRequest + return [ + "isMainFrame": isMainFrame, + "request": request?.toMap(), + "securityOrigin": securityOrigin + ] + } +} diff --git a/macos/Classes/Types/WKNavigationAction.swift b/macos/Classes/Types/WKNavigationAction.swift new file mode 100644 index 00000000..f93080f7 --- /dev/null +++ b/macos/Classes/Types/WKNavigationAction.swift @@ -0,0 +1,28 @@ +// +// WKNavigationAction.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKNavigationAction { + public func toMap () -> [String:Any?] { + var shouldPerformDownload: Bool? = nil + if #available(macOS 11.3, *) { + shouldPerformDownload = self.shouldPerformDownload + } + return [ + "request": request.toMap(), + "isForMainFrame": targetFrame?.isMainFrame ?? false, + "hasGesture": nil, + "isRedirect": nil, + "navigationType": navigationType.rawValue, + "sourceFrame": sourceFrame.toMap(), + "targetFrame": targetFrame?.toMap(), + "shouldPerformDownload": shouldPerformDownload + ] + } +} diff --git a/macos/Classes/Types/WKNavigationResponse.swift b/macos/Classes/Types/WKNavigationResponse.swift new file mode 100644 index 00000000..0e685291 --- /dev/null +++ b/macos/Classes/Types/WKNavigationResponse.swift @@ -0,0 +1,19 @@ +// +// WKNavigationResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKNavigationResponse { + public func toMap () -> [String:Any?] { + return [ + "response": response.toMap(), + "isForMainFrame": isForMainFrame, + "canShowMIMEType": canShowMIMEType, + ] + } +} diff --git a/macos/Classes/Types/WKSecurityOrigin.swift b/macos/Classes/Types/WKSecurityOrigin.swift new file mode 100644 index 00000000..a7e8c714 --- /dev/null +++ b/macos/Classes/Types/WKSecurityOrigin.swift @@ -0,0 +1,20 @@ +// +// WKSecurityOrigin.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +@available(iOS 9.0, *) +extension WKSecurityOrigin { + public func toMap () -> [String:Any?] { + return [ + "host": host, + "port": port, + "protocol": self.protocol + ] + } +} diff --git a/macos/Classes/Types/WKUserContentController.swift b/macos/Classes/Types/WKUserContentController.swift new file mode 100644 index 00000000..fb17db60 --- /dev/null +++ b/macos/Classes/Types/WKUserContentController.swift @@ -0,0 +1,352 @@ +// +// UserContentController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/02/21. +// + +import Foundation +import WebKit +import OrderedSet + +extension WKUserContentController { + static var WINDOW_ID_PREFIX = "WINDOW-ID-" + + // Workaround to create stored properties in an extension: + // https://valv0.medium.com/computed-properties-and-extensions-a-pure-swift-approach-64733768112c + + @available(macOS 11.0, *) + private static var _contentWorlds = [String: Set]() + @available(macOS 11.0, *) + var contentWorlds: Set { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._contentWorlds[tmpAddress] ?? [] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._contentWorlds[tmpAddress] = newValue + } + } + + private static var _userOnlyScripts = [String: [WKUserScriptInjectionTime:OrderedSet]]() + var userOnlyScripts: [WKUserScriptInjectionTime:OrderedSet] { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._userOnlyScripts[tmpAddress] ?? [:] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._userOnlyScripts[tmpAddress] = newValue + } + } + + private static var _pluginScripts = [String: [WKUserScriptInjectionTime:OrderedSet]]() + var pluginScripts: [WKUserScriptInjectionTime:OrderedSet] { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._pluginScripts[tmpAddress] ?? [:] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._pluginScripts[tmpAddress] = newValue + } + } + + public func initialize () { + if #available(macOS 11.0, *) { + contentWorlds = Set([WKContentWorld.page]) + } + pluginScripts = [ + .atDocumentStart: OrderedSet(sequence: []), + .atDocumentEnd: OrderedSet(sequence: []), + ] + userOnlyScripts = [ + .atDocumentStart: OrderedSet(sequence: []), + .atDocumentEnd: OrderedSet(sequence: []), + ] + } + + public func dispose (windowId: Int64?) { + if windowId == nil { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + if #available(macOS 11.0, *) { + contentWorlds.removeAll() + WKUserContentController._contentWorlds.removeValue(forKey: tmpAddress) + } + pluginScripts.removeAll() + WKUserContentController._pluginScripts.removeValue(forKey: tmpAddress) + userOnlyScripts.removeAll() + WKUserContentController._userOnlyScripts.removeValue(forKey: tmpAddress) + } + else if #available(macOS 11.0, *), let windowId = windowId { + let contentWorldsToRemove = contentWorlds.filter({ $0.windowId == windowId }) + for contentWorld in contentWorldsToRemove { + contentWorlds.remove(contentWorld) + removeAllScriptMessageHandlers(from: contentWorld) + } + } + } + + public func sync(scriptMessageHandler: WKScriptMessageHandler) { + let pluginScriptsList = pluginScripts.compactMap({ $0.value }).joined() + for pluginScript in pluginScriptsList { + if !containsPluginScript(with: pluginScript.groupName!) { + addUserScript(pluginScript) + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + add(scriptMessageHandler, name: messageHandlerName) + } + } + if #available(macOS 11.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in contentWorlds { + let pluginScriptWithContentWorld = pluginScript.copyAndSet(contentWorld: contentWorld) + if !containsPluginScript(with: pluginScriptWithContentWorld.groupName!, in: contentWorld) { + addUserScript(pluginScriptWithContentWorld) + for messageHandlerName in pluginScriptWithContentWorld.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + add(scriptMessageHandler, contentWorld: contentWorld, name: messageHandlerName) + } + } + } + } + } + + let userOnlyScriptsList = userOnlyScripts.compactMap({ $0.value }).joined() + for userOnlyScript in userOnlyScriptsList { + if !userScripts.contains(userOnlyScript) { + addUserScript(userOnlyScript) + } + } + } + + public func addUserOnlyScript(_ userOnlyScript: UserScript) { + if #available(macOS 11.0, *) { + contentWorlds.insert(userOnlyScript.contentWorld) + } + userOnlyScripts[userOnlyScript.injectionTime]!.append(userOnlyScript) + } + + public func addUserOnlyScripts(_ userOnlyScripts: [UserScript]) { + for userOnlyScript in userOnlyScripts { + addUserOnlyScript(userOnlyScript) + } + } + + public func addPluginScript(_ pluginScript: PluginScript) { + if #available(macOS 11.0, *) { + contentWorlds.insert(pluginScript.contentWorld) + } + pluginScripts[pluginScript.injectionTime]!.append(pluginScript) + } + + public func addPluginScripts(_ pluginScripts: [PluginScript]) { + for pluginScript in pluginScripts { + addPluginScript(pluginScript) + } + } + + public func getPluginScriptsRequiredInAllContentWorlds() -> [PluginScript] { + return pluginScripts.compactMap({ $0.value }) + .joined() + .filter({ $0.injectionTime == .atDocumentStart && $0.requiredInAllContentWorlds }) + } + + @available(macOS 11.0, *) + public func generateCodeForScriptEvaluation(scriptMessageHandler: WKScriptMessageHandler, source: String, contentWorld: WKContentWorld) -> String { + let (inserted, _) = contentWorlds.insert(contentWorld) + if inserted { + var generatedCode = "" + let pluginScriptsRequired = getPluginScriptsRequiredInAllContentWorlds() + for pluginScript in pluginScriptsRequired { + generatedCode += pluginScript.source + "\n" + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + add(scriptMessageHandler, contentWorld: contentWorld, name: messageHandlerName) + } + } + if let windowId = contentWorld.windowId { + generatedCode += "\(WINDOW_ID_VARIABLE_JS_SOURCE) = \(String(windowId));\n" + } + return generatedCode + "\n" + source + } + return source + } + + public func removeUserOnlyScript(_ userOnlyScript: UserScript) { + userOnlyScripts[userOnlyScript.injectionTime]!.remove(userOnlyScript) + removeUserScript(scriptToRemove: userOnlyScript) + } + + public func removeUserOnlyScript(at index: Int, injectionTime: WKUserScriptInjectionTime) { + let scriptToRemove = userOnlyScripts[injectionTime]![index] + userOnlyScripts[injectionTime]!.removeObject(at: index) + removeUserScript(scriptToRemove: scriptToRemove) + } + + public func removeAllUserOnlyScripts() { + let allUserOnlyScripts = Array(userOnlyScripts.compactMap({ $0.value }).joined()) + + userOnlyScripts[.atDocumentStart]!.removeAllObjects() + userOnlyScripts[.atDocumentEnd]!.removeAllObjects() + + removeUserScripts(scriptsToRemove: allUserOnlyScripts) + } + + public func removePluginScript(_ pluginScript: PluginScript) { + pluginScripts[pluginScript.injectionTime]!.remove(pluginScript) + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + if #available(macOS 11.0, *) { + for contentWorld in contentWorlds { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + } + } + } + removeUserScript(scriptToRemove: pluginScript) + } + + public func removeAllPluginScripts() { + let allPluginScripts = Array(pluginScripts.compactMap({ $0.value }).joined()) + + pluginScripts[.atDocumentStart]!.removeAllObjects() + pluginScripts[.atDocumentEnd]!.removeAllObjects() + + removeUserScripts(scriptsToRemove: allPluginScripts) + } + + public func removeAllPluginScriptMessageHandlers() { + let allPluginScripts = pluginScripts.compactMap({ $0.value }).joined() + for pluginScript in allPluginScripts { + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + } + } + if #available(macOS 11.0, *) { + removeAllScriptMessageHandlers() + for contentWorld in contentWorlds { + removeAllScriptMessageHandlers(from: contentWorld) + } + } + } + + @available(macOS 11.0, *) + public func resetContentWorlds(windowId: Int64?) { + let allUserOnlyScripts = userOnlyScripts.compactMap({ $0.value }).joined() + let contentWorldsFiltered = contentWorlds.filter({ $0.windowId == windowId && $0 != WKContentWorld.page }) + for contentWorld in contentWorldsFiltered { + var found = false + for script in allUserOnlyScripts { + if script.contentWorld == contentWorld { + found = true + break + } + } + if !found { + contentWorlds.remove(contentWorld) + } + } + } + + private func removeUserScript(scriptToRemove: WKUserScript, shouldAddPreviousScripts: Bool = true) -> Void { + // there isn't a way to remove a specific user script using WKUserContentController, + // so we remove all the user scripts and, then, we add them again without the one that has been removed + let userScripts = useCopyOfUserScripts() + + var userScriptsUpdated: [WKUserScript] = [] + for script in userScripts { + if script != scriptToRemove { + userScriptsUpdated.append(script) + } + } + + removeAllUserScripts() + + if shouldAddPreviousScripts { + for script in userScriptsUpdated { + addUserScript(script) + } + } + } + + private func removeUserScripts(scriptsToRemove: [WKUserScript], shouldAddPreviousScripts: Bool = true) -> Void { + // there isn't a way to remove a specific user script using WKUserContentController, + // so we remove all the user scripts and, then, we add them again without the one that has been removed + let userScripts = useCopyOfUserScripts() + + var userScriptsUpdated: [WKUserScript] = [] + for script in userScripts { + if !userScripts.contains(script) { + userScriptsUpdated.append(script) + } + } + + removeAllUserScripts() + + if shouldAddPreviousScripts { + for script in userScriptsUpdated { + addUserScript(script) + } + } + } + + public func removeUserOnlyScripts(with groupName: String, shouldAddPreviousScripts: Bool = true) -> Void { + let allUserOnlyScripts = userOnlyScripts.compactMap({ $0.value }).joined() + var scriptsToRemove: [UserScript] = [] + for script in allUserOnlyScripts { + if let scriptName = script.groupName, scriptName == groupName { + scriptsToRemove.append(script) + } + } + removeUserScripts(scriptsToRemove: scriptsToRemove, shouldAddPreviousScripts: shouldAddPreviousScripts) + } + + public func removePluginScripts(with groupName: String, shouldAddPreviousScripts: Bool = true) -> Void { + let allPluginScripts = pluginScripts.compactMap({ $0.value }).joined() + var scriptsToRemove: [PluginScript] = [] + for script in allPluginScripts { + if let scriptName = script.groupName, scriptName == groupName { + scriptsToRemove.append(script) + } + } + removeUserScripts(scriptsToRemove: scriptsToRemove, shouldAddPreviousScripts: shouldAddPreviousScripts) + } + + public func containsPluginScript(with groupName: String) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script.groupName == groupName { + return true + } + } + return false + } + + @available(macOS 11.0, *) + public func containsPluginScript(with groupName: String, in contentWorld: WKContentWorld) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script.groupName == groupName, script.contentWorld == contentWorld { + return true + } + } + return false + } + + @available(macOS 11.0, *) + public func getContentWorlds(with windowId: Int64?) -> Set { + var contentWorldsFiltered = Set([WKContentWorld.page]) + let contentWorlds = Array(self.contentWorlds) + for contentWorld in contentWorlds { + if contentWorld.windowId == windowId { + contentWorldsFiltered.insert(contentWorld) + } + } + return contentWorldsFiltered + } + + // use a copy of self.userScripts to avoid EXC_BREAKPOINT at runtime if self.userScripts gets removed when another code is looping them + private func useCopyOfUserScripts() -> [WKUserScript] { + return Array(self.userScripts) + } +} diff --git a/macos/Classes/Types/WKWindowFeatures.swift b/macos/Classes/Types/WKWindowFeatures.swift new file mode 100644 index 00000000..469e2be1 --- /dev/null +++ b/macos/Classes/Types/WKWindowFeatures.swift @@ -0,0 +1,24 @@ +// +// WKWindowFeatures.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKWindowFeatures { + public func toMap () -> [String:Any?] { + return [ + "allowsResizing": allowsResizing, + "height": height, + "menuBarVisibility": menuBarVisibility, + "statusBarVisibility": statusBarVisibility, + "toolbarsVisibility": toolbarsVisibility, + "width": width, + "x": x, + "y": y + ] + } +} diff --git a/macos/Classes/Types/WebMessage.swift b/macos/Classes/Types/WebMessage.swift new file mode 100644 index 00000000..378bd67f --- /dev/null +++ b/macos/Classes/Types/WebMessage.swift @@ -0,0 +1,28 @@ +// +// WebMessage.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessage : NSObject { + var data: String? + var ports: [WebMessagePort]? + + public init(data: String?, ports: [WebMessagePort]?) { + super.init() + self.data = data + self.ports = ports + } + + public func dispose() { + ports?.removeAll() + } + + deinit { + debugPrint("WebMessage - dealloc") + dispose() + } +} diff --git a/macos/Classes/Types/WebMessagePort.swift b/macos/Classes/Types/WebMessagePort.swift new file mode 100644 index 00000000..9e823e92 --- /dev/null +++ b/macos/Classes/Types/WebMessagePort.swift @@ -0,0 +1,123 @@ +// +// WebMessagePort.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessagePort : NSObject { + var name: String + var webMessageChannel: WebMessageChannel? + var isClosed = false + var isTransferred = false + var isStarted = false + + public init(name: String, webMessageChannel: WebMessageChannel) { + self.name = name + super.init() + self.webMessageChannel = webMessageChannel + } + + public func setWebMessageCallback(completionHandler: ((Any?) -> Void)? = nil) throws { + if isClosed || isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + self.isStarted = true + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + let index = name == "port1" ? 0 : 1 + webView.evaluateJavascript(source: """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).onmessage = function (event) { + window.webkit.messageHandlers["onWebMessagePortMessageReceived"].postMessage({ + "webMessageChannelId": "\(webMessageChannel.id)", + "index": \(String(index)), + "message": event.data + }); + } + } + })(); + """) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + } + + public func postMessage(message: WebMessage, completionHandler: ((Any?) -> Void)? = nil) throws { + if isClosed || isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + var portsString = "null" + if let ports = message.ports { + var portArrayString: [String] = [] + for port in ports { + if port == self { + throw NSError(domain: "Source port cannot be transferred", code: 0) + } + if port.isStarted { + throw NSError(domain: "Port is already started", code: 0) + } + if port.isClosed || port.isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + port.isTransferred = true + portArrayString.append("\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)['\(port.webMessageChannel!.id)'].\(port.name)") + } + portsString = "[" + portArrayString.joined(separator: ", ") + "]" + } + let data = message.data?.replacingOccurrences(of: "\'", with: "\\'") ?? "null" + let source = """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).postMessage('\(data)', \(portsString)); + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + message.dispose() + } + + public func close(completionHandler: ((Any?) -> Void)? = nil) throws { + if isTransferred { + throw NSError(domain: "Port is already transferred", code: 0) + } + isClosed = true + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + let source = """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).close(); + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + } + + public func dispose() { + isClosed = true + webMessageChannel = nil + } + + deinit { + debugPrint("WebMessagePort - dealloc") + dispose() + } +} diff --git a/macos/Classes/Types/WebResourceError.swift b/macos/Classes/Types/WebResourceError.swift new file mode 100644 index 00000000..3183944b --- /dev/null +++ b/macos/Classes/Types/WebResourceError.swift @@ -0,0 +1,25 @@ +// +// WebResourceError.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation + +public class WebResourceError: NSObject { + var type: Int + var errorDescription: String + + public init(type: Int, errorDescription: String) { + self.type = type + self.errorDescription = errorDescription + } + + public func toMap () -> [String:Any?] { + return [ + "type": type, + "description": errorDescription + ] + } +} diff --git a/macos/Classes/Types/WebResourceRequest.swift b/macos/Classes/Types/WebResourceRequest.swift new file mode 100644 index 00000000..ad7635c4 --- /dev/null +++ b/macos/Classes/Types/WebResourceRequest.swift @@ -0,0 +1,53 @@ +// +// WebResourceRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation +import WebKit + +public class WebResourceRequest: NSObject { + var url: URL + var headers: [AnyHashable:Any]? + var isRedirect = false + var hasGesture = false + var isForMainFrame = true + var method = "GET" + + public init(url: URL, headers: [AnyHashable:Any]?) { + self.url = url + self.headers = headers + } + + public init(url: URL, headers: [AnyHashable:Any]?, isForMainFrame: Bool) { + self.url = url + self.headers = headers + self.isForMainFrame = isForMainFrame + } + + public init(fromURLRequest: URLRequest) { + self.url = fromURLRequest.url ?? URL(string: "about:blank")! + self.headers = fromURLRequest.allHTTPHeaderFields + self.method = fromURLRequest.httpMethod ?? "GET" + } + + public init(fromWKNavigationResponse: WKNavigationResponse) { + let response = fromWKNavigationResponse.response as? HTTPURLResponse + self.url = response?.url ?? URL(string: "about:blank")! + self.headers = response?.allHeaderFields + self.isForMainFrame = fromWKNavigationResponse.isForMainFrame + } + + public func toMap () -> [String:Any?] { + return [ + "url": url.absoluteString, + "headers": headers, + "isRedirect": isRedirect, + "hasGesture": hasGesture, + "isForMainFrame": isForMainFrame, + "method": method + ] + } +} diff --git a/macos/Classes/Types/WebResourceResponse.swift b/macos/Classes/Types/WebResourceResponse.swift new file mode 100644 index 00000000..4fcf13e7 --- /dev/null +++ b/macos/Classes/Types/WebResourceResponse.swift @@ -0,0 +1,47 @@ +// +// WebResourceResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation +import WebKit + +public class WebResourceResponse: NSObject { + var contentType: String + var contentEncoding: String + var data: Data? + var headers: [AnyHashable:Any]? + var statusCode: Int? + var reasonPhrase: String? + + public init(contentType: String, contentEncoding: String, data: Data?, + headers: [AnyHashable:Any]?, statusCode: Int?, reasonPhrase: String?) { + self.contentType = contentType + self.contentEncoding = contentEncoding + self.data = data + self.headers = headers + self.statusCode = statusCode + self.reasonPhrase = reasonPhrase + } + + public init(fromWKNavigationResponse: WKNavigationResponse) { + let response = fromWKNavigationResponse.response as? HTTPURLResponse + self.contentType = response?.mimeType ?? "" + self.contentEncoding = response?.textEncodingName ?? "" + self.headers = response?.allHeaderFields + self.statusCode = response?.statusCode + } + + public func toMap () -> [String:Any?] { + return [ + "contentType": contentType, + "contentEncoding": contentEncoding, + "data": data, + "headers": headers, + "statusCode": statusCode, + "reasonPhrase": reasonPhrase + ] + } +} diff --git a/macos/Classes/Types/WebViewTransport.swift b/macos/Classes/Types/WebViewTransport.swift new file mode 100644 index 00000000..7e21f7e6 --- /dev/null +++ b/macos/Classes/Types/WebViewTransport.swift @@ -0,0 +1,18 @@ +// +// WebViewTransport.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public class WebViewTransport: NSObject { + var webView: InAppWebView + var request: URLRequest + + init(webView: InAppWebView, request: URLRequest) { + self.webView = webView + self.request = request + } +} diff --git a/macos/Classes/Util.swift b/macos/Classes/Util.swift new file mode 100644 index 00000000..d248748e --- /dev/null +++ b/macos/Classes/Util.swift @@ -0,0 +1,140 @@ +// +// Util.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 12/02/21. +// + +import Foundation +import WebKit +import FlutterMacOS + +var SharedLastTouchPointTimestamp: [InAppWebView: Int64] = [:] + +public class Util { + public static func getUrlAsset(assetFilePath: String) throws -> URL { +// let key = SwiftFlutterPlugin.instance?.registrar?.lookupKey(forAsset: assetFilePath) + guard let assetURL = Bundle.main.url(forResource: assetFilePath, withExtension: nil) else { + throw NSError(domain: assetFilePath + " asset file cannot be found!", code: 0) + } + return assetURL + } + + public static func getAbsPathAsset(assetFilePath: String) throws -> String { +// let key = SwiftFlutterPlugin.instance?.registrar?.lookupKey(forAsset: assetFilePath) + guard let assetAbsPath = Bundle.main.path(forResource: assetFilePath, ofType: nil) else { + throw NSError(domain: assetFilePath + " asset file cannot be found!", code: 0) + } + return assetAbsPath + } + + public static func convertToDictionary(text: String) -> [String: Any]? { + if let data = text.data(using: .utf8) { + do { + return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } catch { + print(error.localizedDescription) + } + } + return nil + } + + public static func JSONStringify(value: Any, prettyPrinted: Bool = false) -> String { + let options: JSONSerialization.WritingOptions = prettyPrinted ? .prettyPrinted : .init(rawValue: 0) + if JSONSerialization.isValidJSONObject(value) { + let data = try? JSONSerialization.data(withJSONObject: value, options: options) + if data != nil { + if let string = String(data: data!, encoding: .utf8) { + return string + } + } + } + return "" + } + + @available(macOS 11.0, *) + public static func getContentWorld(name: String) -> WKContentWorld { + switch name { + case "defaultClient": + return WKContentWorld.defaultClient + case "page": + return WKContentWorld.page + default: + return WKContentWorld.world(name: name) + } + } + + public static func isIPv4(address: String) -> Bool { + var sin = sockaddr_in() + return address.withCString({ cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) }) == 1 + } + + public static func isIPv6(address: String) -> Bool { + var sin6 = sockaddr_in6() + return address.withCString({ cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) }) == 1 + } + + public static func isIpAddress(address: String) -> Bool { + return Util.isIPv6(address: address) || Util.isIPv4(address: address) + } + + public static func normalizeIPv6(address: String) throws -> String { + if !Util.isIPv6(address: address) { + throw NSError(domain: "Invalid address: \(address)", code: 0) + } + var ipString = address + // replace ipv4 address if any + let ipv4Regex = try! NSRegularExpression(pattern: "(.*:)([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$)") + if let match = ipv4Regex.firstMatch(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count)) { + if let ipv6PartRange = Range(match.range(at: 1), in: address) { + ipString = String(address[ipv6PartRange]) + } + if let ipv4Range = Range(match.range(at: 2), in: address) { + let ipv4 = address[ipv4Range] + let ipv4Splitted = ipv4.split(separator: ".") + var ipv4Converted = Array(repeating: "0000", count: 4) + for i in 0...3 { + let byte = Int(ipv4Splitted[i])! + let hex = ("0" + String(byte, radix: 16)) + var offset = hex.count - 3 + offset = offset < 0 ? 0 : offset + let fromIndex = hex.index(hex.startIndex, offsetBy: offset) + let toIndex = hex.index(hex.startIndex, offsetBy: hex.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + ipv4Converted[i] = String(hex[indexRange]) + } + ipString += ipv4Converted[0] + ipv4Converted[1] + ":" + ipv4Converted[2] + ipv4Converted[3] + } + } + + // take care of leading and trailing :: + let regex = try! NSRegularExpression(pattern: "^:|:$") + ipString = regex.stringByReplacingMatches(in: ipString, options: [], range: NSRange(location: 0, length: ipString.count), withTemplate: "") + + let ipv6 = ipString.split(separator: ":", omittingEmptySubsequences: false) + var fullIPv6 = Array(repeating: "0000", count: ipv6.count) + + for (i, hex) in ipv6.enumerated() { + if !hex.isEmpty { + // normalize leading zeros + let hexString = String("0000" + hex) + var offset = hexString.count - 5 + offset = offset < 0 ? 0 : offset + let fromIndex = hexString.index(hexString.startIndex, offsetBy: offset) + let toIndex = hexString.index(hexString.startIndex, offsetBy: hexString.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + fullIPv6[i] = String(hexString[indexRange]) + } else { + // normalize grouped zeros :: + var zeros: [String] = [] + for _ in ipv6.count...8 { + zeros.append("0000") + } + fullIPv6[i] = zeros.joined(separator: ":") + } + } + + return fullIPv6.joined(separator: ":") + } + +} diff --git a/macos/Classes/WKProcessPoolManager.swift b/macos/Classes/WKProcessPoolManager.swift new file mode 100755 index 00000000..038fb4d7 --- /dev/null +++ b/macos/Classes/WKProcessPoolManager.swift @@ -0,0 +1,13 @@ +// +// WKProcessPoolManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/11/2019. +// + +import Foundation +import WebKit + +public class WKProcessPoolManager { + static let sharedProcessPool = WKProcessPool() +} diff --git a/macos/Classes/WebAuthenticationSession/WebAuthenticationSession.swift b/macos/Classes/WebAuthenticationSession/WebAuthenticationSession.swift new file mode 100644 index 00000000..f25806e6 --- /dev/null +++ b/macos/Classes/WebAuthenticationSession/WebAuthenticationSession.swift @@ -0,0 +1,99 @@ +// +// WebAuthenticationSession.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import AuthenticationServices +import SafariServices +import FlutterMacOS + +public class WebAuthenticationSession : NSObject, ASWebAuthenticationPresentationContextProviding, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_webauthenticationsession_" + var id: String + var url: URL + var callbackURLScheme: String? + var settings: WebAuthenticationSessionSettings + var session: Any? + var channelDelegate: WebAuthenticationSessionChannelDelegate? + private var _canStart = true + + public init(id: String, url: URL, callbackURLScheme: String?, settings: WebAuthenticationSessionSettings) { + self.id = id + self.url = url + self.settings = settings + super.init() + self.callbackURLScheme = callbackURLScheme + if #available(macOS 10.15, *) { + let session = ASWebAuthenticationSession(url: self.url, callbackURLScheme: self.callbackURLScheme, completionHandler: self.completionHandler) + session.presentationContextProvider = self + self.session = session + } + let channel = FlutterMethodChannel(name: WebAuthenticationSession.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger) + self.channelDelegate = WebAuthenticationSessionChannelDelegate(webAuthenticationSession: self, channel: channel) + } + + public func prepare() { + if #available(macOS 10.15, *), let session = session as? ASWebAuthenticationSession { + session.prefersEphemeralWebBrowserSession = settings.prefersEphemeralWebBrowserSession + } + } + + public func completionHandler(url: URL?, error: Error?) -> Void { + channelDelegate?.onComplete(url: url, errorCode: error?._code) + } + + public func canStart() -> Bool { + guard let session = session else { + return false + } + if #available(macOS 10.15.4, *), let session = session as? ASWebAuthenticationSession { + return session.canStart + } + return _canStart + } + + public func start() -> Bool { + guard let session = session else { + return false + } + var started = false + if #available(macOS 10.15, *), let session = session as? ASWebAuthenticationSession { + started = session.start() + } + if started { + _canStart = false + } + return started + } + + public func cancel() { + guard let session = session else { + return + } + if #available(macOS 10.15, *), let session = session as? ASWebAuthenticationSession { + session.cancel() + } + } + + @available(macOS 10.15, *) + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return NSApplication.shared.windows.first { $0.isKeyWindow } ?? ASPresentationAnchor() + } + + public func dispose() { + cancel() + channelDelegate?.dispose() + channelDelegate = nil + session = nil + WebAuthenticationSessionManager.sessions[id] = nil + } + + deinit { + debugPrint("WebAuthenticationSession - dealloc") + dispose() + } +} diff --git a/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift new file mode 100644 index 00000000..7c476a6a --- /dev/null +++ b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift @@ -0,0 +1,74 @@ +// +// WebAuthenticationSessionChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import FlutterMacOS + +public class WebAuthenticationSessionChannelDelegate : ChannelDelegate { + private weak var webAuthenticationSession: WebAuthenticationSession? + + public init(webAuthenticationSession: WebAuthenticationSession, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webAuthenticationSession = webAuthenticationSession + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + // let arguments = call.arguments as? NSDictionary + switch call.method { + case "canStart": + if let webAuthenticationSession = webAuthenticationSession { + result(webAuthenticationSession.canStart()) + } else { + result(false) + } + break + case "start": + if let webAuthenticationSession = webAuthenticationSession { + result(webAuthenticationSession.start()) + } else { + result(false) + } + break + case "cancel": + if let webAuthenticationSession = webAuthenticationSession { + webAuthenticationSession.cancel() + result(true) + } else { + result(false) + } + break + case "dispose": + if let webAuthenticationSession = webAuthenticationSession { + webAuthenticationSession.dispose() + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onComplete(url: URL?, errorCode: Int?) { + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "errorCode": errorCode + ] + channel?.invokeMethod("onComplete", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webAuthenticationSession = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift new file mode 100644 index 00000000..9f418422 --- /dev/null +++ b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift @@ -0,0 +1,78 @@ +// +// WebAuthenticationSessionManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation +import SafariServices + +public class WebAuthenticationSessionManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_webauthenticationsession" + static var registrar: FlutterPluginRegistrar? + static var sessions: [String: WebAuthenticationSession?] = [:] + + init(registrar: FlutterPluginRegistrar) { + super.init(channel: FlutterMethodChannel(name: WebAuthenticationSessionManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger)) + WebAuthenticationSessionManager.registrar = registrar + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "create": + let id = arguments!["id"] as! String + let url = arguments!["url"] as! String + let callbackURLScheme = arguments!["callbackURLScheme"] as? String + let initialSettings = arguments!["initialSettings"] as! [String: Any?] + create(id: id, url: url, callbackURLScheme: callbackURLScheme, settings: initialSettings, result: result) + break + case "isAvailable": + if #available(iOS 11.0, *) { + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func create(id: String, url: String, callbackURLScheme: String?, settings: [String: Any?], result: @escaping FlutterResult) { + if #available(iOS 11.0, *) { + let sessionUrl = URL(string: url) ?? URL(string: "about:blank")! + let initialSettings = WebAuthenticationSessionSettings() + let _ = initialSettings.parse(settings: settings) + let session = WebAuthenticationSession(id: id, url: sessionUrl, callbackURLScheme: callbackURLScheme, settings: initialSettings) + session.prepare() + WebAuthenticationSessionManager.sessions[id] = session + result(true) + return + } + + result(FlutterError.init(code: "WebAuthenticationSessionManager", message: "WebAuthenticationSession is not available!", details: nil)) + } + + public override func dispose() { + super.dispose() + WebAuthenticationSessionManager.registrar = nil + let sessions = WebAuthenticationSessionManager.sessions.values + sessions.forEach { (session: WebAuthenticationSession?) in + session?.cancel() + session?.dispose() + } + WebAuthenticationSessionManager.sessions.removeAll() + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift new file mode 100644 index 00000000..ac262a31 --- /dev/null +++ b/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift @@ -0,0 +1,28 @@ +// +// WebAuthenticationSessionSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import AuthenticationServices +import SafariServices + +@objcMembers +public class WebAuthenticationSessionSettings: ISettings { + + var prefersEphemeralWebBrowserSession = false + + override init(){ + super.init() + } + + override func getRealSettings(obj: WebAuthenticationSession?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + if #available(macOS 10.15, *), let session = obj?.session as? ASWebAuthenticationSession { + realOptions["prefersEphemeralWebBrowserSession"] = session.prefersEphemeralWebBrowserSession + } + return realOptions + } +} diff --git a/macos/flutter_inappwebview.podspec b/macos/flutter_inappwebview.podspec new file mode 100644 index 00000000..97601f6f --- /dev/null +++ b/macos/flutter_inappwebview.podspec @@ -0,0 +1,27 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_inappwebview.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_inappwebview' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.resources = 'Storyboards/**/*.storyboard' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' + + s.dependency 'OrderedSet', '~>5.0' +end diff --git a/pubspec.yaml b/pubspec.yaml index 73b6b19f..0d6e3792 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_inappwebview description: A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. -version: 6.0.0-beta.2 +version: 6.0.0-beta.3 homepage: https://inappwebview.dev/ repository: https://github.com/pichillilorenzo/flutter_inappwebview issue_tracker: https://github.com/pichillilorenzo/flutter_inappwebview/issues @@ -39,6 +39,8 @@ flutter: pluginClass: InAppWebViewFlutterPlugin ios: pluginClass: InAppWebViewFlutterPlugin + macos: + pluginClass: InAppWebViewFlutterPlugin web: pluginClass: FlutterInAppWebViewWebPlatform fileName: flutter_inappwebview.dart From 3e3ba55a3076653863564c40671542e344bb809a Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Tue, 18 Oct 2022 11:44:08 +0200 Subject: [PATCH 2/9] updated print job controller for macos --- .../types/PrintJobInfoExt.java | 4 +- build.yaml | 5 + .../ios/Flutter/flutter_export_environment.sh | 5 +- .../macos/Runner.xcodeproj/project.pbxproj | 1 - ios/Classes/PrintJob/PrintJobInfo.swift | 4 +- ios/Classes/Types/BaseCallbackResult.swift | 4 +- lib/src/print_job/print_job_settings.dart | 335 ++++++++++++++++-- lib/src/types/main.dart | 4 + lib/src/types/print_job_attributes.dart | 99 +++++- lib/src/types/print_job_attributes.g.dart | 166 ++++++++- lib/src/types/print_job_disposition.dart | 38 ++ lib/src/types/print_job_disposition.g.dart | 127 +++++++ lib/src/types/print_job_info.dart | 82 ++++- lib/src/types/print_job_info.g.dart | 104 +++++- lib/src/types/print_job_orientation.dart | 16 +- lib/src/types/print_job_page_order.dart | 38 ++ lib/src/types/print_job_page_order.g.dart | 135 +++++++ lib/src/types/print_job_pagination_mode.dart | 34 ++ .../types/print_job_pagination_mode.g.dart | 111 ++++++ .../types/print_job_rendering_quality.dart | 9 + .../types/print_job_rendering_quality.g.dart | 33 +- lib/src/types/print_job_state.dart | 14 +- lib/src/types/print_job_state.g.dart | 18 +- lib/src/types/printer.dart | 27 ++ lib/src/types/printer.g.dart | 70 ++++ .../FindInteractionChannelDelegate.swift | 112 ++++++ .../FindInteractionController.swift | 96 +++++ .../FindInteractionSettings.swift | 25 ++ .../InAppBrowser/InAppBrowserManager.swift | 2 + .../InAppBrowserWebViewController.swift | 6 + .../FlutterWebViewController.swift | 6 + macos/Classes/InAppWebView/InAppWebView.swift | 116 +++++- .../InAppWebView/WebViewChannelDelegate.swift | 41 +++ .../WebViewChannelDelegateMethods.swift | 6 + macos/Classes/PrintJob/PrintAttributes.swift | 60 +++- .../Classes/PrintJob/PrintJobController.swift | 1 - macos/Classes/PrintJob/PrintJobInfo.swift | 53 ++- macos/Classes/PrintJob/PrintJobSettings.swift | 182 ++++++---- macos/Classes/Types/BaseCallbackResult.swift | 4 +- macos/Classes/Types/CallbackResult.swift | 4 +- macos/Classes/Types/MethodChannelResult.swift | 4 +- macos/Classes/Types/NSPrinter.swift | 18 + macos/Classes/Util.swift | 15 + 43 files changed, 2072 insertions(+), 162 deletions(-) create mode 100644 build.yaml create mode 100644 lib/src/types/print_job_disposition.dart create mode 100644 lib/src/types/print_job_disposition.g.dart create mode 100644 lib/src/types/print_job_page_order.dart create mode 100644 lib/src/types/print_job_page_order.g.dart create mode 100644 lib/src/types/print_job_pagination_mode.dart create mode 100644 lib/src/types/print_job_pagination_mode.g.dart create mode 100644 lib/src/types/printer.dart create mode 100644 lib/src/types/printer.g.dart create mode 100644 macos/Classes/FindInteraction/FindInteractionChannelDelegate.swift create mode 100644 macos/Classes/FindInteraction/FindInteractionController.swift create mode 100644 macos/Classes/FindInteraction/FindInteractionSettings.swift create mode 100644 macos/Classes/Types/NSPrinter.swift diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/PrintJobInfoExt.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/PrintJobInfoExt.java index f2aafe6e..8c1305b0 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/PrintJobInfoExt.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/PrintJobInfoExt.java @@ -48,7 +48,9 @@ public class PrintJobInfoExt { obj.put("numberOfPages", numberOfPages); obj.put("creationTime", creationTime); obj.put("label", label); - obj.put("printerId", printerId); + Map printer = new HashMap<>(); + printer.put("id", printerId); + obj.put("printer", printer); obj.put("attributes", attributes != null ? attributes.toMap() : null); return obj; } diff --git a/build.yaml b/build.yaml new file mode 100644 index 00000000..e2b3acf3 --- /dev/null +++ b/build.yaml @@ -0,0 +1,5 @@ +targets: + $default: + sources: + exclude: + - example/**.dart diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index 9e98dd5e..fae63896 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -3,12 +3,11 @@ export "FLUTTER_ROOT=/Users/lorenzopichilli/fvm/versions/2.10.4" export "FLUTTER_APPLICATION_PATH=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example" export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/lib/main.dart" +export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" -export "DART_DEFINES=Zmx1dHRlci5pbnNwZWN0b3Iuc3RydWN0dXJlZEVycm9ycz10cnVl,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/.dart_tool/package_config.json" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index f9c8aaaf..f53831be 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -167,7 +167,6 @@ 7D4269DF6E938B573DA3AB1B /* Pods-Runner.release.xcconfig */, B44881B5FC807BDF77BD81E9 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; diff --git a/ios/Classes/PrintJob/PrintJobInfo.swift b/ios/Classes/PrintJob/PrintJobInfo.swift index 9eb8aba1..84a00261 100644 --- a/ios/Classes/PrintJob/PrintJobInfo.swift +++ b/ios/Classes/PrintJob/PrintJobInfo.swift @@ -37,7 +37,9 @@ public class PrintJobInfo : NSObject { "numberOfPages": numberOfPages, "creationTime": creationTime, "label": label, - "printerId": printerId + "printer": [ + "id": printerId + ] ] } } diff --git a/ios/Classes/Types/BaseCallbackResult.swift b/ios/Classes/Types/BaseCallbackResult.swift index f7edb4a5..a76caf72 100644 --- a/ios/Classes/Types/BaseCallbackResult.swift +++ b/ios/Classes/Types/BaseCallbackResult.swift @@ -1,8 +1,8 @@ // // BaseCallbackResult.swift -// flutter_inappwebview +// shared-apple // -// Created by Lorenzo Pichilli on 06/05/22. +// Created by Lorenzo Pichilli on 17/10/22. // import Foundation diff --git a/lib/src/print_job/print_job_settings.dart b/lib/src/print_job/print_job_settings.dart index 26673efc..1c4d7a80 100644 --- a/lib/src/print_job/print_job_settings.dart +++ b/lib/src/print_job/print_job_settings.dart @@ -13,6 +13,7 @@ class PrintJobSettings { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS bool handledByClient; ///The name of the print job. @@ -22,6 +23,7 @@ class PrintJobSettings { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS String? jobName; ///`true` to animate the display of the sheet, `false` to display the sheet immediately. @@ -35,12 +37,14 @@ class PrintJobSettings { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS PrintJobOrientation? orientation; ///The number of pages to render. /// ///**Supported Platforms/Implementations**: ///- iOS + ///- MacOS int? numberOfPages; ///Force rendering quality. @@ -57,6 +61,7 @@ class PrintJobSettings { /// ///**Supported Platforms/Implementations**: ///- iOS + ///- MacOS EdgeInsets? margins; ///The media size. @@ -69,6 +74,7 @@ class PrintJobSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView + ///- MacOS PrintJobColorMode? colorMode; ///The duplex mode to use for the print job. @@ -97,6 +103,7 @@ class PrintJobSettings { /// ///**Supported Platforms/Implementations**: ///- iOS + ///- MacOS bool showsNumberOfCopies; ///A Boolean value that determines whether the paper selection menu displays. @@ -115,8 +122,73 @@ class PrintJobSettings { /// ///**Supported Platforms/Implementations**: ///- iOS + ///- MacOS bool showsPaperOrientation; + ///A Boolean value that determines whether the print panel includes a control for manipulating the paper size of the printer. + ///The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool showsPaperSize; + + ///A Boolean value that determines whether the Print panel includes a control for scaling the printed output. + ///The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool showsScaling; + + ///A Boolean value that determines whether the Print panel includes a set of fields for manipulating the range of pages being printed. + ///The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool showsPageRange; + + ///A Boolean value that determines whether the Print panel includes a separate accessory view for manipulating the paper size, orientation, and scaling attributes. + ///The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool showsPageSetupAccessory; + + ///A Boolean value that determines whether the Print panel displays a built-in preview of the document contents. + ///The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool showsPreview; + + ///A Boolean value that determines whether the Print panel includes an additional selection option for paper range. + ///The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool showsPrintSelection; + + ///A Boolean value that determines whether the print operation displays a print panel. + ///The default value is `true`. + /// + ///This property does not affect the display of a progress panel; + ///that operation is controlled by the [showsProgressPanel] property. + ///Operations that generate EPS or PDF data do no display a progress panel, regardless of the value in the flag parameter. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool showsPrintPanel; + + ///A Boolean value that determines whether the print operation displays a progress panel. + ///The default value is `true`. + /// + ///This property does not affect the display of a print panel; + ///that operation is controlled by the [showsPrintPanel] property. + ///Operations that generate EPS or PDF data do no display a progress panel, regardless of the value in the flag parameter. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool showsProgressPanel; + ///The height of the page footer. /// ///The footer is measured in points from the bottom of [printableRect] and is below the content area. @@ -156,6 +228,132 @@ class PrintJobSettings { ///- iOS double? maximumContentWidth; + ///The current scaling factor. From `0.0` to `1.0`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + double? scalingFactor; + + ///The action specified for the job. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobDisposition? jobDisposition; + + ///An URL containing the location to which the job file will be saved when the [jobDisposition] is [PrintJobDisposition.SAVE]. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + Uri? jobSavingURL; + + ///The name of the currently selected paper size. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + String? paperName; + + ///The horizontal pagination mode. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobPaginationMode? horizontalPagination; + + ///The vertical pagination to the specified mode. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobPaginationMode? verticalPagination; + + ///Indicates whether the image is centered horizontally. + ///The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool isHorizontallyCentered; + + ///Indicates whether the image is centered vertically. + ///The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool isVerticallyCentered; + + ///The print order for the pages of the operation. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobPageOrder? pageOrder; + + ///Whether the print operation should spawn a separate thread in which to run itself. + ///The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool canSpawnSeparateThread; + + ///How many copies to print. + ///The default value is `1`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int copies; + + ///An integer value that specifies the first page in the print job. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? firstPage; + + ///An integer value that specifies the last page in the print job. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? lastPage; + + ///If `true`, produce detailed reports when an error occurs. + ///The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool detailedErrorReporting; + + ///A fax number. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + String? faxNumber; + + ///If `true`, a standard header and footer are added outside the margins of each page. + ///The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool headerAndFooter; + + ///If `true`, collates output. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? mustCollate; + + ///The number of logical pages to be tiled horizontally on a physical sheet of paper. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? pagesAcross; + + ///The number of logical pages to be tiled vertically on a physical sheet of paper. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? pagesDown; + + ///A timestamp that specifies the time at which printing should begin. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? time; + PrintJobSettings( {this.handledByClient = false, this.jobName, @@ -175,35 +373,94 @@ class PrintJobSettings { this.maximumContentHeight, this.maximumContentWidth, this.footerHeight, - this.headerHeight}); + this.headerHeight, + this.showsPaperSize = true, + this.showsScaling = true, + this.showsPageRange = true, + this.showsPageSetupAccessory = true, + this.showsPreview = true, + this.showsPrintSelection = true, + this.scalingFactor, + this.showsPrintPanel = true, + this.showsProgressPanel = true, + this.jobDisposition, + this.jobSavingURL, + this.paperName, + this.horizontalPagination, + this.verticalPagination, + this.isHorizontallyCentered = true, + this.isVerticallyCentered = true, + this.pageOrder, + this.canSpawnSeparateThread = true, + this.copies = 1, + this.firstPage, + this.lastPage, + this.detailedErrorReporting = false, + this.faxNumber, + this.headerAndFooter = true, + this.mustCollate, + this.pagesAcross, + this.pagesDown, + this.time}); ///Gets a [PrintJobSettings] instance from a [Map] value. factory PrintJobSettings.fromMap(Map map) { return PrintJobSettings( - handledByClient: map["handledByClient"], - jobName: map["jobName"], - animated: map["animated"], - orientation: PrintJobOrientation.fromNativeValue(map["orientation"]), - numberOfPages: map["numberOfPages"], - forceRenderingQuality: PrintJobRenderingQuality.fromNativeValue( - map["forceRenderingQuality"]), - margins: MapEdgeInsets.fromMap(map["margins"]?.cast()), - mediaSize: - PrintJobMediaSize.fromMap(map["mediaSize"]?.cast()), - colorMode: PrintJobColorMode.fromNativeValue(map["colorMode"]), - duplexMode: PrintJobDuplexMode.fromNativeValue(map["duplexMode"]), - outputType: PrintJobOutputType.fromNativeValue(map["outputType"]), - resolution: PrintJobResolution.fromMap( - map["resolution"]?.cast()), - showsNumberOfCopies: map["showsNumberOfCopies"], - showsPaperSelectionForLoadedPapers: - map["showsPaperSelectionForLoadedPapers"], - showsPaperOrientation: map["showsPaperOrientation"], - maximumContentHeight: map["maximumContentHeight"], - maximumContentWidth: map["maximumContentWidth"], - footerHeight: map["footerHeight"], - headerHeight: map["headerHeight"], - ); + handledByClient: map["handledByClient"], + jobName: map["jobName"], + animated: map["animated"], + orientation: PrintJobOrientation.fromNativeValue(map["orientation"]), + numberOfPages: map["numberOfPages"], + forceRenderingQuality: PrintJobRenderingQuality.fromNativeValue( + map["forceRenderingQuality"]), + margins: MapEdgeInsets.fromMap(map["margins"]?.cast()), + mediaSize: PrintJobMediaSize.fromMap( + map["mediaSize"]?.cast()), + colorMode: PrintJobColorMode.fromNativeValue(map["colorMode"]), + duplexMode: PrintJobDuplexMode.fromNativeValue(map["duplexMode"]), + outputType: PrintJobOutputType.fromNativeValue(map["outputType"]), + resolution: PrintJobResolution.fromMap( + map["resolution"]?.cast()), + showsNumberOfCopies: map["showsNumberOfCopies"], + showsPaperSelectionForLoadedPapers: + map["showsPaperSelectionForLoadedPapers"], + showsPaperOrientation: map["showsPaperOrientation"], + maximumContentHeight: map["maximumContentHeight"], + maximumContentWidth: map["maximumContentWidth"], + footerHeight: map["footerHeight"], + headerHeight: map["headerHeight"], + showsPaperSize: map["showsPaperSize"], + showsScaling: map["showsScaling"], + showsPageRange: map["showsPageRange"], + showsPageSetupAccessory: map["showsPageSetupAccessory"], + showsPreview: map["showsPreview"], + showsPrintSelection: map["showsPrintSelection"], + scalingFactor: map["scalingFactor"], + showsPrintPanel: map["showsPrintPanel"], + showsProgressPanel: map["showsProgressPanel"], + jobDisposition: + PrintJobDisposition.fromNativeValue(map["jobDisposition"]), + jobSavingURL: + map["jobSavingURL"] != null ? Uri.parse(map["jobSavingURL"]) : null, + paperName: map["paperName"], + horizontalPagination: + PrintJobPaginationMode.fromNativeValue(map["horizontalPagination"]), + verticalPagination: + PrintJobPaginationMode.fromNativeValue(map["verticalPagination"]), + isHorizontallyCentered: map["isHorizontallyCentered"], + isVerticallyCentered: map["isVerticallyCentered"], + pageOrder: PrintJobPageOrder.fromNativeValue(map["pageOrder"]), + canSpawnSeparateThread: map["canSpawnSeparateThread"], + copies: map["copies"], + firstPage: map["firstPage"], + lastPage: map["lastPage"], + detailedErrorReporting: map["detailedErrorReporting"], + faxNumber: map["faxNumber"], + headerAndFooter: map["headerAndFooter"], + mustCollate: map["mustCollate"], + pagesAcross: map["pagesAcross"], + pagesDown: map["pagesDown"], + time: map["time"]); } Map toMap() { @@ -227,6 +484,34 @@ class PrintJobSettings { "maximumContentWidth": maximumContentWidth, "footerHeight": footerHeight, "headerHeight": headerHeight, + "showsPaperSize": showsPaperSize, + "showsScaling": showsScaling, + "showsPageRange": showsPageRange, + "showsPageSetupAccessory": showsPageSetupAccessory, + "showsPreview": showsPreview, + "showsPrintSelection": showsPrintSelection, + "scalingFactor": scalingFactor, + "showsPrintPanel": showsPrintPanel, + "showsProgressPanel": showsProgressPanel, + "jobDisposition": jobDisposition?.toNativeValue(), + "jobSavingURL": jobSavingURL.toString(), + "paperName": paperName, + "horizontalPagination": horizontalPagination?.toNativeValue(), + "verticalPagination": verticalPagination?.toNativeValue(), + "isHorizontallyCentered": isHorizontallyCentered, + "isVerticallyCentered": isVerticallyCentered, + "pageOrder": pageOrder?.toNativeValue(), + "canSpawnSeparateThread": canSpawnSeparateThread, + "copies": copies, + "firstPage": firstPage, + "lastPage": lastPage, + "detailedErrorReporting": detailedErrorReporting, + "faxNumber": faxNumber, + "headerAndFooter": headerAndFooter, + "mustCollate": mustCollate, + "pagesAcross": pagesAcross, + "pagesDown": pagesDown, + "time": time }; } diff --git a/lib/src/types/main.dart b/lib/src/types/main.dart index 84cbe04b..b427d132 100644 --- a/lib/src/types/main.dart +++ b/lib/src/types/main.dart @@ -208,3 +208,7 @@ export 'find_session.dart' show FindSession; export 'search_result_display_style.dart' show SearchResultDisplayStyle; export 'content_blocker_trigger_load_context.dart' show ContentBlockerTriggerLoadContext; +export 'print_job_page_order.dart' show PrintJobPageOrder; +export 'print_job_pagination_mode.dart' show PrintJobPaginationMode; +export 'print_job_disposition.dart' show PrintJobDisposition; +export 'printer.dart' show Printer; diff --git a/lib/src/types/print_job_attributes.dart b/lib/src/types/print_job_attributes.dart index 800a80cd..4ca454e6 100644 --- a/lib/src/types/print_job_attributes.dart +++ b/lib/src/types/print_job_attributes.dart @@ -9,6 +9,8 @@ import 'print_job_duplex_mode.dart'; import 'print_job_orientation.dart'; import 'print_job_media_size.dart'; import 'print_job_resolution.dart'; +import 'print_job_pagination_mode.dart'; +import 'print_job_disposition.dart'; part 'print_job_attributes.g.dart'; @@ -17,12 +19,12 @@ part 'print_job_attributes.g.dart'; @ExchangeableObject() class PrintJobAttributes_ { ///The color mode. - @SupportedPlatforms(platforms: [AndroidPlatform()]) + @SupportedPlatforms(platforms: [AndroidPlatform(), MacOSPlatform()]) PrintJobColorMode_? colorMode; ///The duplex mode to use for the print job. @SupportedPlatforms( - platforms: [AndroidPlatform(available: "23"), IOSPlatform()]) + platforms: [AndroidPlatform(available: "23"), IOSPlatform(), MacOSPlatform()]) PrintJobDuplexMode_? duplex; ///The orientation of the printed content, portrait or landscape. @@ -39,7 +41,7 @@ class PrintJobAttributes_ { ///The margins for each printed page. ///Margins define the white space around the content where the left margin defines ///the amount of white space on the left of the content and so on. - @SupportedPlatforms(platforms: [IOSPlatform()]) + @SupportedPlatforms(platforms: [IOSPlatform(), MacOSPlatform()]) EdgeInsets? margins; ///The height of the page footer. @@ -60,14 +62,14 @@ class PrintJobAttributes_ { /// ///The value of this property is a rectangle that defines the area in which the printer can print content. ///Sometimes this is referred to as the imageable area of the paper. - @SupportedPlatforms(platforms: [IOSPlatform()]) + @SupportedPlatforms(platforms: [IOSPlatform(), MacOSPlatform()]) InAppWebViewRect_? printableRect; ///The size of the paper used for printing. /// ///The value of this property is a rectangle that defines the size of paper chosen for the print job. ///The origin is always (0,0). - @SupportedPlatforms(platforms: [IOSPlatform()]) + @SupportedPlatforms(platforms: [IOSPlatform(), MacOSPlatform()]) InAppWebViewRect_? paperRect; ///The maximum height of the content area. @@ -87,6 +89,74 @@ class PrintJobAttributes_ { @SupportedPlatforms(platforms: [IOSPlatform()]) double? maximumContentWidth; + ///The name of the currently selected paper size. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + String? paperName; + + ///The human-readable name of the currently selected paper size, suitable for presentation in user interfaces. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + String? localizedPaperName; + + ///The horizontal pagination mode. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + PrintJobPaginationMode_? horizontalPagination; + + ///The vertical pagination to the specified mode. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + PrintJobPaginationMode_? verticalPagination; + + ///The action specified for the job. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + PrintJobDisposition_? jobDisposition; + + ///Indicates whether the image is centered horizontally. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + bool? isHorizontallyCentered; + + ///Indicates whether the image is centered vertically. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + bool? isVerticallyCentered; + + ///Indicates whether only the currently selected contents should be printed. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + bool? isSelectionOnly; + + ///The current scaling factor. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + double? scalingFactor; + + ///An URL containing the location to which the job file will be saved when the [jobDisposition] is [PrintJobDisposition.SAVE]. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + Uri? jobSavingURL; + + ///If `true`, produce detailed reports when an error occurs. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + bool? detailedErrorReporting; + + ///A fax number. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + String? faxNumber; + + ///If `true`, a standard header and footer are added outside the margins of each page. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + bool? headerAndFooter; + + ///If `true`, collates output. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + bool? mustCollate; + + ///The number of logical pages to be tiled horizontally on a physical sheet of paper. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + int? pagesAcross; + + ///The number of logical pages to be tiled vertically on a physical sheet of paper. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + int? pagesDown; + + ///A timestamp that specifies the time at which printing should begin. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + int? time; + PrintJobAttributes_( {this.colorMode, this.duplex, @@ -99,5 +169,22 @@ class PrintJobAttributes_ { this.footerHeight, this.headerHeight, this.paperRect, - this.printableRect}); + this.printableRect, + this.paperName, + this.localizedPaperName, + this.horizontalPagination, + this.verticalPagination, + this.jobDisposition, + this.isHorizontallyCentered, + this.isVerticallyCentered, + this.isSelectionOnly, + this.scalingFactor, + this.jobSavingURL, + this.detailedErrorReporting, + this.faxNumber, + this.headerAndFooter, + this.mustCollate, + this.pagesAcross, + this.pagesDown, + this.time}); } diff --git a/lib/src/types/print_job_attributes.g.dart b/lib/src/types/print_job_attributes.g.dart index 16e16fa1..e4218a70 100644 --- a/lib/src/types/print_job_attributes.g.dart +++ b/lib/src/types/print_job_attributes.g.dart @@ -13,6 +13,7 @@ class PrintJobAttributes { /// ///**Supported Platforms/Implementations**: ///- Android native WebView + ///- MacOS PrintJobColorMode? colorMode; ///The duplex mode to use for the print job. @@ -20,6 +21,7 @@ class PrintJobAttributes { ///**Supported Platforms/Implementations**: ///- Android native WebView 23+ ///- iOS + ///- MacOS PrintJobDuplexMode? duplex; ///The orientation of the printed content, portrait or landscape. @@ -43,6 +45,7 @@ class PrintJobAttributes { /// ///**Supported Platforms/Implementations**: ///- iOS + ///- MacOS EdgeInsets? margins; ///The height of the page footer. @@ -70,6 +73,7 @@ class PrintJobAttributes { /// ///**Supported Platforms/Implementations**: ///- iOS + ///- MacOS InAppWebViewRect? printableRect; ///The size of the paper used for printing. @@ -79,6 +83,7 @@ class PrintJobAttributes { /// ///**Supported Platforms/Implementations**: ///- iOS + ///- MacOS InAppWebViewRect? paperRect; ///The maximum height of the content area. @@ -101,6 +106,108 @@ class PrintJobAttributes { ///**Supported Platforms/Implementations**: ///- iOS double? maximumContentWidth; + + ///The name of the currently selected paper size. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + String? paperName; + + ///The human-readable name of the currently selected paper size, suitable for presentation in user interfaces. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + String? localizedPaperName; + + ///The horizontal pagination mode. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobPaginationMode? horizontalPagination; + + ///The vertical pagination to the specified mode. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobPaginationMode? verticalPagination; + + ///The action specified for the job. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobDisposition? jobDisposition; + + ///Indicates whether the image is centered horizontally. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? isHorizontallyCentered; + + ///Indicates whether the image is centered vertically. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? isVerticallyCentered; + + ///Indicates whether only the currently selected contents should be printed. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? isSelectionOnly; + + ///The current scaling factor. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + double? scalingFactor; + + ///An URL containing the location to which the job file will be saved when the [jobDisposition] is [PrintJobDisposition.SAVE]. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + Uri? jobSavingURL; + + ///If `true`, produce detailed reports when an error occurs. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? detailedErrorReporting; + + ///A fax number. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + String? faxNumber; + + ///If `true`, a standard header and footer are added outside the margins of each page. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? headerAndFooter; + + ///If `true`, collates output. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? mustCollate; + + ///The number of logical pages to be tiled horizontally on a physical sheet of paper. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? pagesAcross; + + ///The number of logical pages to be tiled vertically on a physical sheet of paper. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? pagesDown; + + ///A timestamp that specifies the time at which printing should begin. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? time; PrintJobAttributes( {this.colorMode, this.duplex, @@ -113,7 +220,24 @@ class PrintJobAttributes { this.printableRect, this.paperRect, this.maximumContentHeight, - this.maximumContentWidth}); + this.maximumContentWidth, + this.paperName, + this.localizedPaperName, + this.horizontalPagination, + this.verticalPagination, + this.jobDisposition, + this.isHorizontallyCentered, + this.isVerticallyCentered, + this.isSelectionOnly, + this.scalingFactor, + this.jobSavingURL, + this.detailedErrorReporting, + this.faxNumber, + this.headerAndFooter, + this.mustCollate, + this.pagesAcross, + this.pagesDown, + this.time}); ///Gets a possible [PrintJobAttributes] instance from a [Map] value. static PrintJobAttributes? fromMap(Map? map) { @@ -137,6 +261,27 @@ class PrintJobAttributes { InAppWebViewRect.fromMap(map['paperRect']?.cast()), maximumContentHeight: map['maximumContentHeight'], maximumContentWidth: map['maximumContentWidth'], + paperName: map['paperName'], + localizedPaperName: map['localizedPaperName'], + horizontalPagination: + PrintJobPaginationMode.fromNativeValue(map['horizontalPagination']), + verticalPagination: + PrintJobPaginationMode.fromNativeValue(map['verticalPagination']), + jobDisposition: + PrintJobDisposition.fromNativeValue(map['jobDisposition']), + isHorizontallyCentered: map['isHorizontallyCentered'], + isVerticallyCentered: map['isVerticallyCentered'], + isSelectionOnly: map['isSelectionOnly'], + scalingFactor: map['scalingFactor'], + jobSavingURL: + map['jobSavingURL'] != null ? Uri.parse(map['jobSavingURL']) : null, + detailedErrorReporting: map['detailedErrorReporting'], + faxNumber: map['faxNumber'], + headerAndFooter: map['headerAndFooter'], + mustCollate: map['mustCollate'], + pagesAcross: map['pagesAcross'], + pagesDown: map['pagesDown'], + time: map['time'], ); return instance; } @@ -156,6 +301,23 @@ class PrintJobAttributes { "paperRect": paperRect?.toMap(), "maximumContentHeight": maximumContentHeight, "maximumContentWidth": maximumContentWidth, + "paperName": paperName, + "localizedPaperName": localizedPaperName, + "horizontalPagination": horizontalPagination?.toNativeValue(), + "verticalPagination": verticalPagination?.toNativeValue(), + "jobDisposition": jobDisposition?.toNativeValue(), + "isHorizontallyCentered": isHorizontallyCentered, + "isVerticallyCentered": isVerticallyCentered, + "isSelectionOnly": isSelectionOnly, + "scalingFactor": scalingFactor, + "jobSavingURL": jobSavingURL?.toString(), + "detailedErrorReporting": detailedErrorReporting, + "faxNumber": faxNumber, + "headerAndFooter": headerAndFooter, + "mustCollate": mustCollate, + "pagesAcross": pagesAcross, + "pagesDown": pagesDown, + "time": time, }; } @@ -166,6 +328,6 @@ class PrintJobAttributes { @override String toString() { - return 'PrintJobAttributes{colorMode: $colorMode, duplex: $duplex, orientation: $orientation, mediaSize: $mediaSize, resolution: $resolution, margins: $margins, footerHeight: $footerHeight, headerHeight: $headerHeight, printableRect: $printableRect, paperRect: $paperRect, maximumContentHeight: $maximumContentHeight, maximumContentWidth: $maximumContentWidth}'; + return 'PrintJobAttributes{colorMode: $colorMode, duplex: $duplex, orientation: $orientation, mediaSize: $mediaSize, resolution: $resolution, margins: $margins, footerHeight: $footerHeight, headerHeight: $headerHeight, printableRect: $printableRect, paperRect: $paperRect, maximumContentHeight: $maximumContentHeight, maximumContentWidth: $maximumContentWidth, paperName: $paperName, localizedPaperName: $localizedPaperName, horizontalPagination: $horizontalPagination, verticalPagination: $verticalPagination, jobDisposition: $jobDisposition, isHorizontallyCentered: $isHorizontallyCentered, isVerticallyCentered: $isVerticallyCentered, isSelectionOnly: $isSelectionOnly, scalingFactor: $scalingFactor, jobSavingURL: $jobSavingURL, detailedErrorReporting: $detailedErrorReporting, faxNumber: $faxNumber, headerAndFooter: $headerAndFooter, mustCollate: $mustCollate, pagesAcross: $pagesAcross, pagesDown: $pagesDown, time: $time}'; } } diff --git a/lib/src/types/print_job_disposition.dart b/lib/src/types/print_job_disposition.dart new file mode 100644 index 00000000..7f788009 --- /dev/null +++ b/lib/src/types/print_job_disposition.dart @@ -0,0 +1,38 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; + +import '../print_job/main.dart'; + +part 'print_job_disposition.g.dart'; + +///Class representing the constants that specify values for the print job disposition of a [PrintJobController]. +@ExchangeableEnum() +class PrintJobDisposition_ { + // ignore: unused_field + final String _value; + const PrintJobDisposition_._internal(this._value); + + ///Normal print job. + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: 'spool') + ]) + static const SPOOL = const PrintJobDisposition_._internal('SPOOL'); + + ///Send to Preview application. + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: 'preview') + ]) + static const PREVIEW = const PrintJobDisposition_._internal("PREVIEW"); + + ///Save to a file. + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: 'save') + ]) + static const SAVE = const PrintJobDisposition_._internal("SAVE"); + + ///Cancel print job. + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: 'cancel') + ]) + static const CANCEL = const PrintJobDisposition_._internal("CANCEL"); +} diff --git a/lib/src/types/print_job_disposition.g.dart b/lib/src/types/print_job_disposition.g.dart new file mode 100644 index 00000000..a48ca101 --- /dev/null +++ b/lib/src/types/print_job_disposition.g.dart @@ -0,0 +1,127 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'print_job_disposition.dart'; + +// ************************************************************************** +// ExchangeableEnumGenerator +// ************************************************************************** + +///Class representing the constants that specify values for the print job disposition of a [PrintJobController]. +class PrintJobDisposition { + final String _value; + final String _nativeValue; + const PrintJobDisposition._internal(this._value, this._nativeValue); +// ignore: unused_element + factory PrintJobDisposition._internalMultiPlatform( + String value, Function nativeValue) => + PrintJobDisposition._internal(value, nativeValue()); + + ///Normal print job. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final SPOOL = PrintJobDisposition._internalMultiPlatform('SPOOL', () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return 'spool'; + default: + break; + } + return null; + }); + + ///Send to Preview application. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final PREVIEW = + PrintJobDisposition._internalMultiPlatform('PREVIEW', () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return 'preview'; + default: + break; + } + return null; + }); + + ///Save to a file. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final SAVE = PrintJobDisposition._internalMultiPlatform('SAVE', () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return 'save'; + default: + break; + } + return null; + }); + + ///Cancel print job. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final CANCEL = + PrintJobDisposition._internalMultiPlatform('CANCEL', () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return 'cancel'; + default: + break; + } + return null; + }); + + ///Set of all values of [PrintJobDisposition]. + static final Set values = [ + PrintJobDisposition.SPOOL, + PrintJobDisposition.PREVIEW, + PrintJobDisposition.SAVE, + PrintJobDisposition.CANCEL, + ].toSet(); + + ///Gets a possible [PrintJobDisposition] instance from [String] value. + static PrintJobDisposition? fromValue(String? value) { + if (value != null) { + try { + return PrintJobDisposition.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets a possible [PrintJobDisposition] instance from a native value. + static PrintJobDisposition? fromNativeValue(String? value) { + if (value != null) { + try { + return PrintJobDisposition.values + .firstWhere((element) => element.toNativeValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets [String] value. + String toValue() => _value; + + ///Gets [String] native value. + String toNativeValue() => _nativeValue; + + @override + int get hashCode => _value.hashCode; + + @override + bool operator ==(value) => value == _value; + + @override + String toString() { + return _value; + } +} diff --git a/lib/src/types/print_job_info.dart b/lib/src/types/print_job_info.dart index ec5e0b43..c544d02d 100644 --- a/lib/src/types/print_job_info.dart +++ b/lib/src/types/print_job_info.dart @@ -2,7 +2,10 @@ import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_i import '../print_job/main.dart'; import 'print_job_attributes.dart'; +import 'print_job_rendering_quality.dart'; import 'print_job_state.dart'; +import 'print_job_page_order.dart'; +import 'printer.dart'; part 'print_job_info.g.dart'; @@ -15,12 +18,14 @@ class PrintJobInfo_ { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS PrintJobState_? state; ///How many copies to print. /// ///**Supported Platforms/Implementations**: ///- Android native WebView + ///- MacOS int? copies; ///The number of pages to print. @@ -28,6 +33,7 @@ class PrintJobInfo_ { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS int? numberOfPages; ///The timestamp when the print job was created. @@ -35,6 +41,7 @@ class PrintJobInfo_ { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS int? creationTime; ///The human readable print job label. @@ -42,20 +49,80 @@ class PrintJobInfo_ { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS String? label; - ///The unique id of the printer. + ///The printer object to be used for printing. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - String? printerId; + ///- MacOS + Printer_? printer; + + ///The page order that will be used to generate the pages in this job. + ///This is the physical page order of the pages. + ///It depends on the stacking order of the printer, the capability of the app to reverse page order, etc. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobPageOrder_? pageOrder; + + ///The printing quality. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobRenderingQuality_? preferredRenderingQuality; + + ///Whether the progress panel is shown during the operation. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? showsProgressPanel; + + ///Whether the print panel is shown during the operation. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? showsPrintPanel; + + ///Whether the print operation should spawn a separate thread in which to run itself. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? canSpawnSeparateThread; + + ///A Boolean value that indicates whether the print operation is an EPS or PDF copy operation. + ///It's `true` if the receiver is an EPS or PDF copy operation; otherwise, `false`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? isCopyingOperation; + + ///The current page number being previewed or printed. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? currentPage; + + ///An integer value that specifies the first page in the print job. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? firstPage; + + ///An integer value that specifies the last page in the print job. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? lastPage; ///The attributes of a print job. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS PrintJobAttributes_? attributes; PrintJobInfo_( @@ -64,6 +131,15 @@ class PrintJobInfo_ { this.numberOfPages, this.creationTime, this.label, - this.printerId, + this.printer, + this.pageOrder, + this.preferredRenderingQuality, + this.showsProgressPanel, + this.showsPrintPanel, + this.canSpawnSeparateThread, + this.isCopyingOperation, + this.currentPage, + this.firstPage, + this.lastPage, this.attributes}); } diff --git a/lib/src/types/print_job_info.g.dart b/lib/src/types/print_job_info.g.dart index ef8ca5c5..97670a03 100644 --- a/lib/src/types/print_job_info.g.dart +++ b/lib/src/types/print_job_info.g.dart @@ -14,12 +14,14 @@ class PrintJobInfo { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS PrintJobState? state; ///How many copies to print. /// ///**Supported Platforms/Implementations**: ///- Android native WebView + ///- MacOS int? copies; ///The number of pages to print. @@ -27,6 +29,7 @@ class PrintJobInfo { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS int? numberOfPages; ///The timestamp when the print job was created. @@ -34,6 +37,7 @@ class PrintJobInfo { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS int? creationTime; ///The human readable print job label. @@ -41,20 +45,80 @@ class PrintJobInfo { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS String? label; - ///The unique id of the printer. + ///The printer object to be used for printing. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - String? printerId; + ///- MacOS + Printer? printer; + + ///The page order that will be used to generate the pages in this job. + ///This is the physical page order of the pages. + ///It depends on the stacking order of the printer, the capability of the app to reverse page order, etc. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobPageOrder? pageOrder; + + ///The printing quality. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + PrintJobRenderingQuality? preferredRenderingQuality; + + ///Whether the progress panel is shown during the operation. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? showsProgressPanel; + + ///Whether the print panel is shown during the operation. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? showsPrintPanel; + + ///Whether the print operation should spawn a separate thread in which to run itself. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? canSpawnSeparateThread; + + ///A Boolean value that indicates whether the print operation is an EPS or PDF copy operation. + ///It's `true` if the receiver is an EPS or PDF copy operation; otherwise, `false`. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + bool? isCopyingOperation; + + ///The current page number being previewed or printed. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? currentPage; + + ///An integer value that specifies the first page in the print job. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? firstPage; + + ///An integer value that specifies the last page in the print job. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? lastPage; ///The attributes of a print job. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS PrintJobAttributes? attributes; PrintJobInfo( {this.state, @@ -62,7 +126,16 @@ class PrintJobInfo { this.numberOfPages, this.creationTime, this.label, - this.printerId, + this.printer, + this.pageOrder, + this.preferredRenderingQuality, + this.showsProgressPanel, + this.showsPrintPanel, + this.canSpawnSeparateThread, + this.isCopyingOperation, + this.currentPage, + this.firstPage, + this.lastPage, this.attributes}); ///Gets a possible [PrintJobInfo] instance from a [Map] value. @@ -76,7 +149,17 @@ class PrintJobInfo { numberOfPages: map['numberOfPages'], creationTime: map['creationTime'], label: map['label'], - printerId: map['printerId'], + printer: Printer.fromMap(map['printer']?.cast()), + pageOrder: PrintJobPageOrder.fromNativeValue(map['pageOrder']), + preferredRenderingQuality: PrintJobRenderingQuality.fromNativeValue( + map['preferredRenderingQuality']), + showsProgressPanel: map['showsProgressPanel'], + showsPrintPanel: map['showsPrintPanel'], + canSpawnSeparateThread: map['canSpawnSeparateThread'], + isCopyingOperation: map['isCopyingOperation'], + currentPage: map['currentPage'], + firstPage: map['firstPage'], + lastPage: map['lastPage'], attributes: PrintJobAttributes.fromMap( map['attributes']?.cast()), ); @@ -91,7 +174,16 @@ class PrintJobInfo { "numberOfPages": numberOfPages, "creationTime": creationTime, "label": label, - "printerId": printerId, + "printer": printer?.toMap(), + "pageOrder": pageOrder?.toNativeValue(), + "preferredRenderingQuality": preferredRenderingQuality?.toNativeValue(), + "showsProgressPanel": showsProgressPanel, + "showsPrintPanel": showsPrintPanel, + "canSpawnSeparateThread": canSpawnSeparateThread, + "isCopyingOperation": isCopyingOperation, + "currentPage": currentPage, + "firstPage": firstPage, + "lastPage": lastPage, "attributes": attributes?.toMap(), }; } @@ -103,6 +195,6 @@ class PrintJobInfo { @override String toString() { - return 'PrintJobInfo{state: $state, copies: $copies, numberOfPages: $numberOfPages, creationTime: $creationTime, label: $label, printerId: $printerId, attributes: $attributes}'; + return 'PrintJobInfo{state: $state, copies: $copies, numberOfPages: $numberOfPages, creationTime: $creationTime, label: $label, printer: $printer, pageOrder: $pageOrder, preferredRenderingQuality: $preferredRenderingQuality, showsProgressPanel: $showsProgressPanel, showsPrintPanel: $showsPrintPanel, canSpawnSeparateThread: $canSpawnSeparateThread, isCopyingOperation: $isCopyingOperation, currentPage: $currentPage, firstPage: $firstPage, lastPage: $lastPage, attributes: $attributes}'; } } diff --git a/lib/src/types/print_job_orientation.dart b/lib/src/types/print_job_orientation.dart index 0694737d..b43d7d26 100644 --- a/lib/src/types/print_job_orientation.dart +++ b/lib/src/types/print_job_orientation.dart @@ -14,23 +14,15 @@ class PrintJobOrientation_ { ///Pages are printed in portrait orientation. @EnumSupportedPlatforms(platforms: [ - EnumIOSPlatform( - value: 0 - ), - EnumMacOSPlatform( - value: 0 - ) + EnumIOSPlatform(value: 0), + EnumMacOSPlatform(value: 0) ]) static const PORTRAIT = const PrintJobOrientation_._internal(0); ///Pages are printed in landscape orientation. @EnumSupportedPlatforms(platforms: [ - EnumIOSPlatform( - value: 1 - ), - EnumMacOSPlatform( - value: 1 - ) + EnumIOSPlatform(value: 1), + EnumMacOSPlatform(value: 1) ]) static const LANDSCAPE = const PrintJobOrientation_._internal(1); } diff --git a/lib/src/types/print_job_page_order.dart b/lib/src/types/print_job_page_order.dart new file mode 100644 index 00000000..386dfe6d --- /dev/null +++ b/lib/src/types/print_job_page_order.dart @@ -0,0 +1,38 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; + +import '../print_job/main.dart'; + +part 'print_job_page_order.g.dart'; + +///Class representing the page order that will be used to generate the pages of a [PrintJobController]. +@ExchangeableEnum() +class PrintJobPageOrder_ { + // ignore: unused_field + final int _value; + const PrintJobPageOrder_._internal(this._value); + + ///Descending (front to back) page order. + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: -1) + ]) + static const DESCENDING = const PrintJobPageOrder_._internal(-1); + + ///The spooler does not rearrange pages—they are printed in the order received by the spooler. + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: 0) + ]) + static const SPECIAL = const PrintJobPageOrder_._internal(0); + + ///Ascending (back to front) page order. + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: 1) + ]) + static const ASCENDING = const PrintJobPageOrder_._internal(1); + + ///No page order specified. + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: 2) + ]) + static const UNKNOWN = const PrintJobPageOrder_._internal(2); +} diff --git a/lib/src/types/print_job_page_order.g.dart b/lib/src/types/print_job_page_order.g.dart new file mode 100644 index 00000000..26598972 --- /dev/null +++ b/lib/src/types/print_job_page_order.g.dart @@ -0,0 +1,135 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'print_job_page_order.dart'; + +// ************************************************************************** +// ExchangeableEnumGenerator +// ************************************************************************** + +///Class representing the page order that will be used to generate the pages of a [PrintJobController]. +class PrintJobPageOrder { + final int _value; + final int _nativeValue; + const PrintJobPageOrder._internal(this._value, this._nativeValue); +// ignore: unused_element + factory PrintJobPageOrder._internalMultiPlatform( + int value, Function nativeValue) => + PrintJobPageOrder._internal(value, nativeValue()); + + ///Descending (front to back) page order. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final DESCENDING = PrintJobPageOrder._internalMultiPlatform(-1, () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return -1; + default: + break; + } + return null; + }); + + ///The spooler does not rearrange pages—they are printed in the order received by the spooler. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final SPECIAL = PrintJobPageOrder._internalMultiPlatform(0, () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return 0; + default: + break; + } + return null; + }); + + ///Ascending (back to front) page order. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final ASCENDING = PrintJobPageOrder._internalMultiPlatform(1, () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return 1; + default: + break; + } + return null; + }); + + ///No page order specified. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final UNKNOWN = PrintJobPageOrder._internalMultiPlatform(2, () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return 2; + default: + break; + } + return null; + }); + + ///Set of all values of [PrintJobPageOrder]. + static final Set values = [ + PrintJobPageOrder.DESCENDING, + PrintJobPageOrder.SPECIAL, + PrintJobPageOrder.ASCENDING, + PrintJobPageOrder.UNKNOWN, + ].toSet(); + + ///Gets a possible [PrintJobPageOrder] instance from [int] value. + static PrintJobPageOrder? fromValue(int? value) { + if (value != null) { + try { + return PrintJobPageOrder.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets a possible [PrintJobPageOrder] instance from a native value. + static PrintJobPageOrder? fromNativeValue(int? value) { + if (value != null) { + try { + return PrintJobPageOrder.values + .firstWhere((element) => element.toNativeValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets [int] value. + int toValue() => _value; + + ///Gets [int] native value. + int toNativeValue() => _nativeValue; + + @override + int get hashCode => _value.hashCode; + + @override + bool operator ==(value) => value == _value; + + @override + String toString() { + switch (_value) { + case -1: + return 'DESCENDING'; + case 0: + return 'SPECIAL'; + case 1: + return 'ASCENDING'; + case 2: + return 'UNKNOWN'; + } + return _value.toString(); + } +} diff --git a/lib/src/types/print_job_pagination_mode.dart b/lib/src/types/print_job_pagination_mode.dart new file mode 100644 index 00000000..90bfad7d --- /dev/null +++ b/lib/src/types/print_job_pagination_mode.dart @@ -0,0 +1,34 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; + +import '../print_job/main.dart'; + +part 'print_job_pagination_mode.g.dart'; + +///Class representing the constants that specify the different ways in which an image is divided into pages of a [PrintJobController]. +@ExchangeableEnum() +class PrintJobPaginationMode_ { + // ignore: unused_field + final String _value; + // ignore: unused_field + final int? _nativeValue = null; + const PrintJobPaginationMode_._internal(this._value); + + /// + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: 0) + ]) + static const AUTOMATIC = const PrintJobPaginationMode_._internal('AUTOMATIC'); + + /// + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: 1) + ]) + static const FIT = const PrintJobPaginationMode_._internal("FIT"); + + /// + @EnumSupportedPlatforms(platforms: [ + EnumMacOSPlatform(value: 2) + ]) + static const CLIP = const PrintJobPaginationMode_._internal("CLIP"); +} diff --git a/lib/src/types/print_job_pagination_mode.g.dart b/lib/src/types/print_job_pagination_mode.g.dart new file mode 100644 index 00000000..9b75951a --- /dev/null +++ b/lib/src/types/print_job_pagination_mode.g.dart @@ -0,0 +1,111 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'print_job_pagination_mode.dart'; + +// ************************************************************************** +// ExchangeableEnumGenerator +// ************************************************************************** + +///Class representing the constants that specify the different ways in which an image is divided into pages of a [PrintJobController]. +class PrintJobPaginationMode { + final String _value; + final int? _nativeValue; + const PrintJobPaginationMode._internal(this._value, this._nativeValue); +// ignore: unused_element + factory PrintJobPaginationMode._internalMultiPlatform( + String value, Function nativeValue) => + PrintJobPaginationMode._internal(value, nativeValue()); + + /// + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final AUTOMATIC = + PrintJobPaginationMode._internalMultiPlatform('AUTOMATIC', () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return 0; + default: + break; + } + return null; + }); + + /// + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final FIT = PrintJobPaginationMode._internalMultiPlatform('FIT', () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return 1; + default: + break; + } + return null; + }); + + /// + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + static final CLIP = PrintJobPaginationMode._internalMultiPlatform('CLIP', () { + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + return 2; + default: + break; + } + return null; + }); + + ///Set of all values of [PrintJobPaginationMode]. + static final Set values = [ + PrintJobPaginationMode.AUTOMATIC, + PrintJobPaginationMode.FIT, + PrintJobPaginationMode.CLIP, + ].toSet(); + + ///Gets a possible [PrintJobPaginationMode] instance from [String] value. + static PrintJobPaginationMode? fromValue(String? value) { + if (value != null) { + try { + return PrintJobPaginationMode.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets a possible [PrintJobPaginationMode] instance from a native value. + static PrintJobPaginationMode? fromNativeValue(int? value) { + if (value != null) { + try { + return PrintJobPaginationMode.values + .firstWhere((element) => element.toNativeValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets [String] value. + String toValue() => _value; + + ///Gets [int?] native value. + int? toNativeValue() => _nativeValue; + + @override + int get hashCode => _value.hashCode; + + @override + bool operator ==(value) => value == _value; + + @override + String toString() { + return _value; + } +} diff --git a/lib/src/types/print_job_rendering_quality.dart b/lib/src/types/print_job_rendering_quality.dart index 9f25ecd5..e765a3dc 100644 --- a/lib/src/types/print_job_rendering_quality.dart +++ b/lib/src/types/print_job_rendering_quality.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; import '../print_job/main.dart'; @@ -12,9 +13,17 @@ class PrintJobRenderingQuality_ { const PrintJobRenderingQuality_._internal(this._value); ///Renders the printing at the best possible quality, regardless of speed. + @EnumSupportedPlatforms(platforms: [ + EnumIOSPlatform(value: 0), + EnumMacOSPlatform(value: 0) + ]) static const BEST = const PrintJobRenderingQuality_._internal(0); ///Sacrifices the least possible amount of rendering quality for speed to maintain a responsive user interface. ///This option should be used only after establishing that best quality rendering does indeed make the user interface unresponsive. + @EnumSupportedPlatforms(platforms: [ + EnumIOSPlatform(value: 1), + EnumMacOSPlatform(value: 1) + ]) static const RESPONSIVE = const PrintJobRenderingQuality_._internal(1); } diff --git a/lib/src/types/print_job_rendering_quality.g.dart b/lib/src/types/print_job_rendering_quality.g.dart index 94a9534d..b836c8fc 100644 --- a/lib/src/types/print_job_rendering_quality.g.dart +++ b/lib/src/types/print_job_rendering_quality.g.dart @@ -17,11 +17,40 @@ class PrintJobRenderingQuality { PrintJobRenderingQuality._internal(value, nativeValue()); ///Renders the printing at the best possible quality, regardless of speed. - static const BEST = PrintJobRenderingQuality._internal(0, 0); + /// + ///**Supported Platforms/Implementations**: + ///- iOS + ///- MacOS + static final BEST = PrintJobRenderingQuality._internalMultiPlatform(0, () { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return 0; + case TargetPlatform.macOS: + return 0; + default: + break; + } + return null; + }); ///Sacrifices the least possible amount of rendering quality for speed to maintain a responsive user interface. ///This option should be used only after establishing that best quality rendering does indeed make the user interface unresponsive. - static const RESPONSIVE = PrintJobRenderingQuality._internal(1, 1); + /// + ///**Supported Platforms/Implementations**: + ///- iOS + ///- MacOS + static final RESPONSIVE = + PrintJobRenderingQuality._internalMultiPlatform(1, () { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return 1; + case TargetPlatform.macOS: + return 1; + default: + break; + } + return null; + }); ///Set of all values of [PrintJobRenderingQuality]. static final Set values = [ diff --git a/lib/src/types/print_job_state.dart b/lib/src/types/print_job_state.dart index beab9e07..84f5438f 100644 --- a/lib/src/types/print_job_state.dart +++ b/lib/src/types/print_job_state.dart @@ -10,6 +10,8 @@ part 'print_job_state.g.dart'; class PrintJobState_ { // ignore: unused_field final int _value; + // ignore: unused_field + final int? _nativeValue = null; const PrintJobState_._internal(this._value); ///Print job state: The print job is being created but not yet ready to be printed. @@ -21,7 +23,8 @@ class PrintJobState_ { apiUrl: 'https://developer.android.com/reference/android/print/PrintJobInfo#STATE_CREATED', value: 1), - EnumIOSPlatform(value: 1) + EnumIOSPlatform(value: 1), + EnumMacOSPlatform(value: 1) ]) static const CREATED = const PrintJobState_._internal(1); @@ -46,7 +49,8 @@ class PrintJobState_ { apiUrl: 'https://developer.android.com/reference/android/print/PrintJobInfo#STATE_STARTED', value: 3), - EnumIOSPlatform(value: 3) + EnumIOSPlatform(value: 3), + EnumMacOSPlatform(value: 3) ]) static const STARTED = const PrintJobState_._internal(3); @@ -71,7 +75,8 @@ class PrintJobState_ { apiUrl: 'https://developer.android.com/reference/android/print/PrintJobInfo#STATE_COMPLETED', value: 5), - EnumIOSPlatform(value: 5) + EnumIOSPlatform(value: 5), + EnumMacOSPlatform(value: 5) ]) static const COMPLETED = const PrintJobState_._internal(5); @@ -97,7 +102,8 @@ class PrintJobState_ { apiUrl: 'https://developer.android.com/reference/android/print/PrintJobInfo#STATE_CANCELED', value: 7), - EnumIOSPlatform(value: 7) + EnumIOSPlatform(value: 7), + EnumMacOSPlatform(value: 7) ]) static const CANCELED = const PrintJobState_._internal(7); } diff --git a/lib/src/types/print_job_state.g.dart b/lib/src/types/print_job_state.g.dart index 06f4ad2d..e5ac9e19 100644 --- a/lib/src/types/print_job_state.g.dart +++ b/lib/src/types/print_job_state.g.dart @@ -9,7 +9,7 @@ part of 'print_job_state.dart'; ///Class representing the state of a [PrintJobController]. class PrintJobState { final int _value; - final int _nativeValue; + final int? _nativeValue; const PrintJobState._internal(this._value, this._nativeValue); // ignore: unused_element factory PrintJobState._internalMultiPlatform( @@ -23,12 +23,15 @@ class PrintJobState { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - PrintJobInfo.STATE_CREATED](https://developer.android.com/reference/android/print/PrintJobInfo#STATE_CREATED)) ///- iOS + ///- MacOS static final CREATED = PrintJobState._internalMultiPlatform(1, () { switch (defaultTargetPlatform) { case TargetPlatform.android: return 1; case TargetPlatform.iOS: return 1; + case TargetPlatform.macOS: + return 1; default: break; } @@ -58,12 +61,15 @@ class PrintJobState { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - PrintJobInfo.STATE_STARTED](https://developer.android.com/reference/android/print/PrintJobInfo#STATE_STARTED)) ///- iOS + ///- MacOS static final STARTED = PrintJobState._internalMultiPlatform(3, () { switch (defaultTargetPlatform) { case TargetPlatform.android: return 3; case TargetPlatform.iOS: return 3; + case TargetPlatform.macOS: + return 3; default: break; } @@ -93,12 +99,15 @@ class PrintJobState { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - PrintJobInfo.STATE_COMPLETED](https://developer.android.com/reference/android/print/PrintJobInfo#STATE_COMPLETED)) ///- iOS + ///- MacOS static final COMPLETED = PrintJobState._internalMultiPlatform(5, () { switch (defaultTargetPlatform) { case TargetPlatform.android: return 5; case TargetPlatform.iOS: return 5; + case TargetPlatform.macOS: + return 5; default: break; } @@ -131,12 +140,15 @@ class PrintJobState { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - PrintJobInfo.STATE_CANCELED](https://developer.android.com/reference/android/print/PrintJobInfo#STATE_CANCELED)) ///- iOS + ///- MacOS static final CANCELED = PrintJobState._internalMultiPlatform(7, () { switch (defaultTargetPlatform) { case TargetPlatform.android: return 7; case TargetPlatform.iOS: return 7; + case TargetPlatform.macOS: + return 7; default: break; } @@ -183,8 +195,8 @@ class PrintJobState { ///Gets [int] value. int toValue() => _value; - ///Gets [int] native value. - int toNativeValue() => _nativeValue; + ///Gets [int?] native value. + int? toNativeValue() => _nativeValue; @override int get hashCode => _value.hashCode; diff --git a/lib/src/types/printer.dart b/lib/src/types/printer.dart new file mode 100644 index 00000000..e54a99e4 --- /dev/null +++ b/lib/src/types/printer.dart @@ -0,0 +1,27 @@ +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; + +import '../print_job/main.dart'; + +part 'printer.g.dart'; + +///Class representing the printer used by a [PrintJobController]. +@ExchangeableObject() +class Printer_ { + ///The unique id of the printer. + @SupportedPlatforms(platforms: [AndroidPlatform(), IOSPlatform()]) + String? id; + + ///A description of the printer’s make and model. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + String? type; + + ///The PostScript language level recognized by the printer. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + int? languageLevel; + + ///The printer’s name. + @SupportedPlatforms(platforms: [MacOSPlatform()]) + String? name; + + Printer_({this.id, this.type, this.languageLevel, this.name}); +} diff --git a/lib/src/types/printer.g.dart b/lib/src/types/printer.g.dart new file mode 100644 index 00000000..4ca68f98 --- /dev/null +++ b/lib/src/types/printer.g.dart @@ -0,0 +1,70 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'printer.dart'; + +// ************************************************************************** +// ExchangeableObjectGenerator +// ************************************************************************** + +///Class representing the printer used by a [PrintJobController]. +class Printer { + ///The unique id of the printer. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + String? id; + + ///A description of the printer’s make and model. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + String? type; + + ///The PostScript language level recognized by the printer. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + int? languageLevel; + + ///The printer’s name. + /// + ///**Supported Platforms/Implementations**: + ///- MacOS + String? name; + Printer({this.id, this.type, this.languageLevel, this.name}); + + ///Gets a possible [Printer] instance from a [Map] value. + static Printer? fromMap(Map? map) { + if (map == null) { + return null; + } + final instance = Printer( + id: map['id'], + type: map['type'], + languageLevel: map['languageLevel'], + name: map['name'], + ); + return instance; + } + + ///Converts instance to a map. + Map toMap() { + return { + "id": id, + "type": type, + "languageLevel": languageLevel, + "name": name, + }; + } + + ///Converts instance to a map. + Map toJson() { + return toMap(); + } + + @override + String toString() { + return 'Printer{id: $id, type: $type, languageLevel: $languageLevel, name: $name}'; + } +} diff --git a/macos/Classes/FindInteraction/FindInteractionChannelDelegate.swift b/macos/Classes/FindInteraction/FindInteractionChannelDelegate.swift new file mode 100644 index 00000000..fe1ab455 --- /dev/null +++ b/macos/Classes/FindInteraction/FindInteractionChannelDelegate.swift @@ -0,0 +1,112 @@ +// +// FindInteractionChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation +import FlutterMacOS + +public class FindInteractionChannelDelegate : ChannelDelegate { + private weak var findInteractionController: FindInteractionController? + + public init(findInteractionController: FindInteractionController, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.findInteractionController = findInteractionController + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "findAll": + if let findInteractionController = findInteractionController { + let find = arguments!["find"] as! String + findInteractionController.findAll(find: find, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "findNext": + if let findInteractionController = findInteractionController { + let forward = arguments!["forward"] as! Bool + findInteractionController.findNext(forward: forward, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "clearMatches": + if let findInteractionController = findInteractionController { + findInteractionController.clearMatches(completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "setSearchText": + if let findInteractionController = findInteractionController { + let searchText = arguments!["searchText"] as? String + findInteractionController.searchText = searchText + result(true) + } else { + result(false) + } + break + case "getSearchText": + result(findInteractionController?.searchText) + break + case "getActiveFindSession": + if let findInteractionController = findInteractionController { + result(findInteractionController.activeFindSession?.toMap()) + } else { + result(nil) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onFindResultReceived(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Bool) { + if isDoneCounting, let findInteractionController = findInteractionController { + findInteractionController.activeFindSession = FindSession(resultCount: numberOfMatches, + highlightedResultIndex: activeMatchOrdinal, + searchResultDisplayStyle: 2) // matches UIFindSession.SearchResultDisplayStyle.none + } + + let arguments: [String : Any?] = [ + "activeMatchOrdinal": activeMatchOrdinal, + "numberOfMatches": numberOfMatches, + "isDoneCounting": isDoneCounting + ] + channel?.invokeMethod("onFindResultReceived", arguments: arguments) + } + + public override func dispose() { + super.dispose() + findInteractionController = nil + } + + deinit { + dispose() + } +} diff --git a/macos/Classes/FindInteraction/FindInteractionController.swift b/macos/Classes/FindInteraction/FindInteractionController.swift new file mode 100644 index 00000000..d1973db5 --- /dev/null +++ b/macos/Classes/FindInteraction/FindInteractionController.swift @@ -0,0 +1,96 @@ +// +// FindInteractionController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation +import FlutterMacOS + +public class FindInteractionController : NSObject, Disposable { + + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_find_interaction_"; + var webView: InAppWebView? + var channelDelegate: FindInteractionChannelDelegate? + var settings: FindInteractionSettings? + var shouldCallOnRefresh = false + var searchText: String? + var activeFindSession: FindSession? + + public init(registrar: FlutterPluginRegistrar, id: Any, webView: InAppWebView, settings: FindInteractionSettings?) { + super.init() + self.webView = webView + self.settings = settings + let channel = FlutterMethodChannel(name: FindInteractionController.METHOD_CHANNEL_NAME_PREFIX + String(describing: id), + binaryMessenger: registrar.messenger) + self.channelDelegate = FindInteractionChannelDelegate(findInteractionController: self, channel: channel) + } + + public func prepare() { +// if let settings = settings { +// +// } + } + + public func findAll(find: String?, completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + + var find = find + if find == nil { + find = searchText + } else { + // updated searchText + searchText = find + } + + guard let find else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + + if find != "" { + let startSearch = "window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync('\(find)');" + webView.evaluateJavaScript(startSearch, completionHandler: completionHandler) + } + } + + public func findNext(forward: Bool, completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + webView.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findNext(\(forward ? "true" : "false"));", completionHandler: completionHandler) + } + + public func clearMatches(completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + webView.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches();", completionHandler: completionHandler) + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + webView = nil + activeFindSession = nil + } + + deinit { + debugPrint("FindInteractionControl - dealloc") + dispose() + } +} diff --git a/macos/Classes/FindInteraction/FindInteractionSettings.swift b/macos/Classes/FindInteraction/FindInteractionSettings.swift new file mode 100644 index 00000000..1c34c5bd --- /dev/null +++ b/macos/Classes/FindInteraction/FindInteractionSettings.swift @@ -0,0 +1,25 @@ +// +// FindInteractionSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +public class FindInteractionSettings : ISettings { + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> FindInteractionSettings { + let _ = super.parse(settings: settings) + return self + } + + override func getRealSettings(obj: FindInteractionController?) -> [String: Any?] { + let realSettings: [String: Any?] = toMap() + return realSettings + } +} diff --git a/macos/Classes/InAppBrowser/InAppBrowserManager.swift b/macos/Classes/InAppBrowser/InAppBrowserManager.swift index 8811fa67..c1e9f629 100755 --- a/macos/Classes/InAppBrowser/InAppBrowserManager.swift +++ b/macos/Classes/InAppBrowser/InAppBrowserManager.swift @@ -84,6 +84,8 @@ public class InAppBrowserManager: ChannelDelegate { if browserSettings.hidden { window.hide() + } else { + window.makeKeyAndOrderFront(self) } } diff --git a/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift index a0982ffa..284b9db4 100755 --- a/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift +++ b/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -62,6 +62,12 @@ public class InAppBrowserWebViewController: NSViewController, InAppBrowserDelega webView.id = id webView.channelDelegate = WebViewChannelDelegate(webView: webView, channel: channel) + let findInteractionController = FindInteractionController( + registrar: SwiftFlutterPlugin.instance!.registrar!, + id: id, webView: webView, settings: nil) + webView.findInteractionController = findInteractionController + findInteractionController.prepare() + prepareWebView() webView.windowCreated = true diff --git a/macos/Classes/InAppWebView/FlutterWebViewController.swift b/macos/Classes/InAppWebView/FlutterWebViewController.swift index e4b7e702..69206434 100755 --- a/macos/Classes/InAppWebView/FlutterWebViewController.swift +++ b/macos/Classes/InAppWebView/FlutterWebViewController.swift @@ -54,6 +54,12 @@ public class FlutterWebViewController: NSObject, /*FlutterPlatformView,*/ Dispos userScripts: userScripts) } + let findInteractionController = FindInteractionController( + registrar: SwiftFlutterPlugin.instance!.registrar!, + id: viewId, webView: webView!, settings: nil) + webView!.findInteractionController = findInteractionController + findInteractionController.prepare() + webView!.autoresizingMask = [.width, .height] myView!.autoresizesSubviews = true myView!.autoresizingMask = [.width, .height] diff --git a/macos/Classes/InAppWebView/InAppWebView.swift b/macos/Classes/InAppWebView/InAppWebView.swift index c8894aad..9236eeef 100755 --- a/macos/Classes/InAppWebView/InAppWebView.swift +++ b/macos/Classes/InAppWebView/InAppWebView.swift @@ -21,6 +21,7 @@ public class InAppWebView: WKWebView, WKUIDelegate, var inAppBrowserDelegate: InAppBrowserDelegate? var channelDelegate: WebViewChannelDelegate? var settings: InAppWebViewSettings? + var findInteractionController: FindInteractionController? var webMessageChannels: [String:WebMessageChannel] = [:] var webMessageListeners: [WebMessageListener] = [] var currentOriginalUrl: URL? @@ -1650,7 +1651,7 @@ public class InAppWebView: WKWebView, WKUIDelegate, let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "") let alert = NSAlert() - alert.messageText = title ?? "" + alert.messageText = dialogMessage ?? "" alert.alertStyle = .informational alert.addButton(withTitle: okButton ?? "") alert.addButton(withTitle: cancelButton ?? "") @@ -1702,7 +1703,7 @@ public class InAppWebView: WKWebView, WKUIDelegate, let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "") let alert = NSAlert() - alert.messageText = title ?? "" + alert.messageText = dialogMessage ?? "" alert.alertStyle = .informational alert.addButton(withTitle: okButton ?? "") alert.addButton(withTitle: cancelButton ?? "") @@ -2113,6 +2114,7 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { webView = webViewTransport.webView } + webView.findInteractionController?.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) webView.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) } else if message.name == "onScrollChanged" { let body = message.body as! [String: Any?] @@ -2212,7 +2214,36 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { printJobId = NSUUID().uuidString } - let printInfo = NSPrintInfo() + var printInfoDictionary: [NSPrintInfo.AttributeKey : Any] = [:] + if let settings = settings { + if let jobSavingURL = settings.jobSavingURL, let url = URL(string: jobSavingURL) { + printInfoDictionary[.jobSavingURL] = url + } + printInfoDictionary[.copies] = settings.copies + if let firstPage = settings.firstPage { + printInfoDictionary[.firstPage] = firstPage + } + if let lastPage = settings.lastPage { + printInfoDictionary[.lastPage] = lastPage + } + printInfoDictionary[.detailedErrorReporting] = settings.detailedErrorReporting + printInfoDictionary[.faxNumber] = settings.faxNumber ?? "" + printInfoDictionary[.headerAndFooter] = settings.headerAndFooter + if let mustCollate = settings.mustCollate { + printInfoDictionary[.mustCollate] = mustCollate + } + if let pagesAcross = settings.pagesAcross { + printInfoDictionary[.pagesAcross] = pagesAcross + } + if let pagesDown = settings.pagesDown { + printInfoDictionary[.pagesDown] = pagesDown + } + if let time = settings.time { + printInfoDictionary[.time] = Date(timeIntervalSince1970: TimeInterval(Double(time)/1000)) + } + } + + let printInfo = NSPrintInfo(dictionary: printInfoDictionary) if let settings = settings { if let orientationValue = settings.orientation, @@ -2225,13 +2256,84 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { printInfo.bottomMargin = margins.bottom printInfo.leftMargin = margins.left } + if let numberOfPages = settings.numberOfPages { + printInfo.printSettings["com_apple_print_PrintSettings_PMLastPage"] = numberOfPages + } + if let colorMode = settings.colorMode { + printInfo.printSettings["ColorModel"] = colorMode + } + if let scalingFactor = settings.scalingFactor { + printInfo.scalingFactor = scalingFactor + } + if let jobDisposition = settings.jobDisposition { + printInfo.jobDisposition = Util.getNSPrintInfoJobDisposition(name: jobDisposition) + } + if let paperName = settings.paperName { + printInfo.paperName = NSPrinter.PaperName.init(rawValue: paperName) + } + if let horizontalPagination = settings.horizontalPagination, + let pagination = NSPrintInfo.PaginationMode.init(rawValue: horizontalPagination) { + printInfo.horizontalPagination = pagination + } + if let verticalPagination = settings.verticalPagination, + let pagination = NSPrintInfo.PaginationMode.init(rawValue: verticalPagination) { + printInfo.verticalPagination = pagination + } + printInfo.isHorizontallyCentered = settings.isHorizontallyCentered + printInfo.isVerticallyCentered = settings.isVerticallyCentered } let printOperation = printOperation(with: printInfo) printOperation.jobTitle = settings?.jobName ?? (title ?? url?.absoluteString ?? "") + " Document" printOperation.view?.frame = bounds - printOperation.printPanel.options.insert(.showsOrientation) - printOperation.printPanel.options.insert(.showsPaperSize) - printOperation.printPanel.options.insert(.showsScaling) + + if let settings = settings { + if let pageOrder = settings.pageOrder, let order = NSPrintOperation.PageOrder.init(rawValue: pageOrder) { + printOperation.pageOrder = order + } + printOperation.canSpawnSeparateThread = settings.canSpawnSeparateThread + printOperation.showsPrintPanel = settings.showsPrintPanel + printOperation.showsProgressPanel = settings.showsProgressPanel + if settings.showsPaperOrientation { + printOperation.printPanel.options.insert(.showsOrientation) + } else { + printOperation.printPanel.options.remove(.showsOrientation) + } + if settings.showsNumberOfCopies { + printOperation.printPanel.options.insert(.showsCopies) + } else { + printOperation.printPanel.options.remove(.showsCopies) + } + if settings.showsPaperSize { + printOperation.printPanel.options.insert(.showsPaperSize) + } else { + printOperation.printPanel.options.remove(.showsPaperSize) + } + if settings.showsScaling { + printOperation.printPanel.options.insert(.showsScaling) + } else { + printOperation.printPanel.options.remove(.showsScaling) + } + if settings.showsPageRange { + printOperation.printPanel.options.insert(.showsPageRange) + } else { + printOperation.printPanel.options.remove(.showsPageRange) + } + if settings.showsPageSetupAccessory { + printOperation.printPanel.options.insert(.showsPageSetupAccessory) + } else { + printOperation.printPanel.options.remove(.showsPageSetupAccessory) + } + if settings.showsPreview { + printOperation.printPanel.options.insert(.showsPreview) + } else { + printOperation.printPanel.options.remove(.showsPreview) + } + if settings.showsPrintSelection { + printOperation.printPanel.options.insert(.showsPrintSelection) + } else { + printOperation.printPanel.options.remove(.showsPrintSelection) + } + } if let id = printJobId { let printJob = PrintJobController(id: id, job: printOperation, settings: settings) @@ -2471,6 +2573,8 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { for imp in customIMPs { imp_removeBlock(imp) } + findInteractionController?.dispose() + findInteractionController = nil uiDelegate = nil navigationDelegate = nil isPausedTimersCompletionHandler = nil diff --git a/macos/Classes/InAppWebView/WebViewChannelDelegate.swift b/macos/Classes/InAppWebView/WebViewChannelDelegate.swift index b32b9baa..b2ec738c 100644 --- a/macos/Classes/InAppWebView/WebViewChannelDelegate.swift +++ b/macos/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -207,6 +207,47 @@ public class WebViewChannelDelegate : ChannelDelegate { case .getCopyBackForwardList: result(webView?.getCopyBackForwardList()) break + case .findAll: + if let webView = webView, let findInteractionController = webView.findInteractionController { + let find = arguments!["find"] as! String + findInteractionController.findAll(find: find, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case .findNext: + if let webView = webView, let findInteractionController = webView.findInteractionController { + let forward = arguments!["forward"] as! Bool + findInteractionController.findNext(forward: forward, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case .clearMatches: + if let webView = webView, let findInteractionController = webView.findInteractionController { + findInteractionController.clearMatches(completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break case .clearCache: webView?.clearCache() result(true) diff --git a/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift b/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift index da6dba91..658c096f 100644 --- a/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift +++ b/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift @@ -35,6 +35,12 @@ public enum WebViewChannelDelegateMethods: String { case show = "show" case hide = "hide" case getCopyBackForwardList = "getCopyBackForwardList" + @available(*, deprecated, message: "Use FindInteractionController.findAll instead.") + case findAll = "findAll" + @available(*, deprecated, message: "Use FindInteractionController.findNext instead.") + case findNext = "findNext" + @available(*, deprecated, message: "Use FindInteractionController.clearMatches instead.") + case clearMatches = "clearMatches" case clearCache = "clearCache" case scrollTo = "scrollTo" case scrollBy = "scrollBy" diff --git a/macos/Classes/PrintJob/PrintAttributes.swift b/macos/Classes/PrintJob/PrintAttributes.swift index 21178457..c6cfdd6b 100644 --- a/macos/Classes/PrintJob/PrintAttributes.swift +++ b/macos/Classes/PrintJob/PrintAttributes.swift @@ -13,11 +13,30 @@ public class PrintAttributes : NSObject { var paperRect: CGRect? var colorMode: String? var duplex: Int? + var paperName: String? + var localizedPaperName: String? + var horizontalPagination: UInt? + var verticalPagination: UInt? + var jobDisposition: String? + var printableRect: NSRect? + var isHorizontallyCentered: Bool? + var isVerticallyCentered: Bool? + var isSelectionOnly: Bool? + var scalingFactor: CGFloat? + var jobSavingURL: String? + var detailedErrorReporting: Bool? + var faxNumber: String? + var headerAndFooter: Bool? + var mustCollate: Bool? + var pagesAcross: Int? + var pagesDown: Int? + var time: Int? public init(fromPrintJobController: PrintJobController) { super.init() if let job = fromPrintJobController.job { let printInfo = job.printInfo + let printInfoDictionary = printInfo.dictionary() orientation = printInfo.orientation margins = NSEdgeInsets(top: printInfo.topMargin, left: printInfo.leftMargin, @@ -26,7 +45,26 @@ public class PrintAttributes : NSObject { paperRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: printInfo.paperSize) colorMode = printInfo.printSettings["ColorModel"] as? String duplex = printInfo.printSettings["com_apple_print_PrintSettings_PMDuplexing"] as? Int - print(printInfo.printSettings) + paperName = printInfo.paperName?.rawValue + localizedPaperName = printInfo.localizedPaperName + horizontalPagination = printInfo.horizontalPagination.rawValue + verticalPagination = printInfo.verticalPagination.rawValue + jobDisposition = printInfo.jobDisposition.rawValue + printableRect = printInfo.imageablePageBounds + isHorizontallyCentered = printInfo.isHorizontallyCentered + isVerticallyCentered = printInfo.isVerticallyCentered + isSelectionOnly = printInfo.isSelectionOnly + scalingFactor = printInfo.scalingFactor + jobSavingURL = (printInfoDictionary[NSPrintInfo.AttributeKey.jobSavingURL] as? URL)?.absoluteString + detailedErrorReporting = printInfoDictionary[NSPrintInfo.AttributeKey.detailedErrorReporting] as? Bool + faxNumber = printInfoDictionary[NSPrintInfo.AttributeKey.faxNumber] as? String + headerAndFooter = printInfoDictionary[NSPrintInfo.AttributeKey.headerAndFooter] as? Bool + mustCollate = printInfoDictionary[NSPrintInfo.AttributeKey.mustCollate] as? Bool + pagesAcross = printInfoDictionary[NSPrintInfo.AttributeKey.pagesAcross] as? Int + pagesDown = printInfoDictionary[NSPrintInfo.AttributeKey.pagesDown] as? Int + if let timestamp = (printInfoDictionary[NSPrintInfo.AttributeKey.time] as? Date)?.timeIntervalSince1970 { + time = Int(timestamp) + } } } @@ -36,7 +74,25 @@ public class PrintAttributes : NSObject { "margins": margins?.toMap(), "orientation": orientation?.rawValue, "colorMode": colorMode, - "duplex": duplex + "duplex": duplex, + "paperName": paperName, + "localizedPaperName": localizedPaperName, + "horizontalPagination": horizontalPagination, + "verticalPagination": verticalPagination, + "jobDisposition": jobDisposition, + "printableRect": printableRect?.toMap(), + "isHorizontallyCentered": isHorizontallyCentered, + "isVerticallyCentered": isVerticallyCentered, + "isSelectionOnly": isSelectionOnly, + "scalingFactor": scalingFactor, + "jobSavingURL": jobSavingURL, + "detailedErrorReporting": detailedErrorReporting, + "faxNumber": faxNumber, + "headerAndFooter": headerAndFooter, + "mustCollate": mustCollate, + "pagesAcross": pagesAcross, + "pagesDown": pagesDown, + "time": time ] } } diff --git a/macos/Classes/PrintJob/PrintJobController.swift b/macos/Classes/PrintJob/PrintJobController.swift index 30637c2b..998038e9 100644 --- a/macos/Classes/PrintJob/PrintJobController.swift +++ b/macos/Classes/PrintJob/PrintJobController.swift @@ -12,7 +12,6 @@ public enum PrintJobState: Int { case created = 1 case started = 3 case completed = 5 - case failed = 6 case canceled = 7 } diff --git a/macos/Classes/PrintJob/PrintJobInfo.swift b/macos/Classes/PrintJob/PrintJobInfo.swift index 6937c077..d522558a 100644 --- a/macos/Classes/PrintJob/PrintJobInfo.swift +++ b/macos/Classes/PrintJob/PrintJobInfo.swift @@ -14,27 +14,46 @@ public class PrintJobInfo : NSObject { var numberOfPages: Int? var copies: Int? var label: String? - var printerName: String? - var printerType: String? + var printer: NSPrinter? + var pageOrder: Int? + var preferredRenderingQuality: Int? + var showsProgressPanel: Bool? + var showsPrintPanel: Bool? + var canSpawnSeparateThread: Bool? + var isCopyingOperation: Bool? + var currentPage: Int? + var firstPage: Int? + var lastPage: Int? public init(fromPrintJobController: PrintJobController) { state = fromPrintJobController.state creationTime = fromPrintJobController.creationTime attributes = PrintAttributes.init(fromPrintJobController: fromPrintJobController) - if let job = fromPrintJobController.job { - let printInfo = job.printInfo - printerName = printInfo.printer.name - printerType = printInfo.printer.type.rawValue - copies = printInfo.printSettings["com_apple_print_PrintSettings_PMCopies"] as? Int - } super.init() if let job = fromPrintJobController.job { let printInfo = job.printInfo + let printInfoDictionary = printInfo.dictionary() + printer = printInfo.printer + copies = printInfo.printSettings["com_apple_print_PrintSettings_PMCopies"] as? Int label = job.jobTitle - numberOfPages = printInfo.printSettings["com_apple_print_PrintSettings_PMLastPage"] as? Int - if numberOfPages == nil || numberOfPages! > job.pageRange.length { - numberOfPages = job.pageRange.length + firstPage = printInfoDictionary[NSPrintInfo.AttributeKey.firstPage] as? Int + lastPage = printInfoDictionary[NSPrintInfo.AttributeKey.lastPage] as? Int + if let firstPage = firstPage, let lastPage = lastPage { + numberOfPages = lastPage - firstPage + 1 } + if numberOfPages == nil { + numberOfPages = printInfo.printSettings["com_apple_print_PrintSettings_PMLastPage"] as? Int + if numberOfPages == nil || numberOfPages! > job.pageRange.length { + numberOfPages = job.pageRange.length + } + } + pageOrder = job.pageOrder.rawValue + preferredRenderingQuality = job.preferredRenderingQuality.rawValue + showsProgressPanel = job.showsProgressPanel + showsPrintPanel = job.showsPrintPanel + canSpawnSeparateThread = job.canSpawnSeparateThread + isCopyingOperation = job.isCopyingOperation + currentPage = job.currentPage } } @@ -46,8 +65,16 @@ public class PrintJobInfo : NSObject { "copies": copies, "creationTime": creationTime, "label": label, - "printerName": printerName, - "printerType": printerType + "printer": printer?.toMap(), + "pageOrder": pageOrder, + "preferredRenderingQuality": preferredRenderingQuality, + "showsProgressPanel": showsProgressPanel, + "showsPrintPanel": showsPrintPanel, + "canSpawnSeparateThread": canSpawnSeparateThread, + "isCopyingOperation": isCopyingOperation, + "currentPage": currentPage, + "firstPage": firstPage, + "lastPage": lastPage ] } } diff --git a/macos/Classes/PrintJob/PrintJobSettings.swift b/macos/Classes/PrintJob/PrintJobSettings.swift index 15abee8c..a3fe36b4 100644 --- a/macos/Classes/PrintJob/PrintJobSettings.swift +++ b/macos/Classes/PrintJob/PrintJobSettings.swift @@ -12,7 +12,6 @@ public class PrintJobSettings: ISettings { public var handledByClient = false public var jobName: String? - public var animated = true public var _orientation: NSNumber? public var orientation: Int? { get { @@ -27,9 +26,9 @@ public class PrintJobSettings: ISettings { } } public var _numberOfPages: NSNumber? - public var numberOfPages: Int? { + public var numberOfPages: Int64? { get { - return _numberOfPages?.intValue + return _numberOfPages?.int64Value } set { if let newValue = newValue { @@ -39,98 +38,155 @@ public class PrintJobSettings: ISettings { } } } - public var _forceRenderingQuality: NSNumber? - public var forceRenderingQuality: Int? { - get { - return _forceRenderingQuality?.intValue - } - set { - if let newValue = newValue { - _forceRenderingQuality = NSNumber.init(value: newValue) - } else { - _forceRenderingQuality = nil - } - } - } public var margins: NSEdgeInsets? - public var _duplexMode: NSNumber? - public var duplexMode: Int? { - get { - return _duplexMode?.intValue - } - set { - if let newValue = newValue { - _duplexMode = NSNumber.init(value: newValue) - } else { - _duplexMode = nil - } - } - } - public var _outputType: NSNumber? - public var outputType: Int? { - get { - return _outputType?.intValue - } - set { - if let newValue = newValue { - _outputType = NSNumber.init(value: newValue) - } else { - _outputType = nil - } - } - } + public var colorMode: String? public var showsNumberOfCopies = true - public var showsPaperSelectionForLoadedPapers = false public var showsPaperOrientation = true - public var _maximumContentHeight: NSNumber? - public var maximumContentHeight: Double? { + public var showsPaperSize = true + public var showsScaling = true + public var showsPageRange = true + public var showsPageSetupAccessory = true + public var showsPreview = true + public var showsPrintSelection = true + public var showsPrintPanel = true + public var showsProgressPanel = true + public var _scalingFactor: NSNumber? + public var scalingFactor: Double? { get { - return _maximumContentHeight?.doubleValue + return _scalingFactor?.doubleValue } set { if let newValue = newValue { - _maximumContentHeight = NSNumber.init(value: newValue) + _scalingFactor = NSNumber.init(value: newValue) } else { - _maximumContentHeight = nil + _scalingFactor = nil } } } - public var _maximumContentWidth: NSNumber? - public var maximumContentWidth: Double? { + public var jobDisposition: String? + public var jobSavingURL: String? + public var paperName: String? + public var _horizontalPagination: NSNumber? + public var horizontalPagination: UInt? { get { - return _maximumContentWidth?.doubleValue + return _horizontalPagination?.uintValue } set { if let newValue = newValue { - _maximumContentWidth = NSNumber.init(value: newValue) + _horizontalPagination = NSNumber.init(value: newValue) } else { - _maximumContentWidth = nil + _horizontalPagination = nil } } } - public var _footerHeight: NSNumber? - public var footerHeight: Double? { + public var _verticalPagination: NSNumber? + public var verticalPagination: UInt? { get { - return _footerHeight?.doubleValue + return _verticalPagination?.uintValue } set { if let newValue = newValue { - _footerHeight = NSNumber.init(value: newValue) + _verticalPagination = NSNumber.init(value: newValue) } else { - _footerHeight = nil + _verticalPagination = nil } } } - public var _headerHeight: NSNumber? - public var headerHeight: Double? { + public var isHorizontallyCentered = true + public var isVerticallyCentered = true + public var _pageOrder: NSNumber? + public var pageOrder: Int? { get { - return _headerHeight?.doubleValue + return _pageOrder?.intValue } set { if let newValue = newValue { - _headerHeight = NSNumber.init(value: newValue) + _pageOrder = NSNumber.init(value: newValue) } else { - _headerHeight = nil + _pageOrder = nil + } + } + } + public var canSpawnSeparateThread = true + public var copies = 1 + public var _firstPage: NSNumber? + public var firstPage: Int64? { + get { + return _firstPage?.int64Value + } + set { + if let newValue = newValue { + _firstPage = NSNumber.init(value: newValue) + } else { + _firstPage = nil + } + } + } + public var _lastPage: NSNumber? + public var lastPage: Int64? { + get { + return _lastPage?.int64Value + } + set { + if let newValue = newValue { + _lastPage = NSNumber.init(value: newValue) + } else { + _lastPage = nil + } + } + } + public var detailedErrorReporting = false + public var faxNumber: String? + public var headerAndFooter = true + public var _mustCollate: NSNumber? + public var mustCollate: Bool? { + get { + return _mustCollate?.boolValue + } + set { + if let newValue = newValue { + _mustCollate = NSNumber.init(value: newValue) + } else { + _mustCollate = nil + } + } + } + public var _pagesAcross: NSNumber? + public var pagesAcross: Int64? { + get { + return _pagesAcross?.int64Value + } + set { + if let newValue = newValue { + _pagesAcross = NSNumber.init(value: newValue) + } else { + _pagesAcross = nil + } + } + } + public var _pagesDown: NSNumber? + public var pagesDown: Int64? { + get { + return _pagesDown?.int64Value + } + set { + if let newValue = newValue { + _pagesDown = NSNumber.init(value: newValue) + } else { + _pagesDown = nil + } + } + } + public var _time: NSNumber? + public var time: Int64? { + get { + return _time?.int64Value + } + set { + if let newValue = newValue { + _time = NSNumber.init(value: newValue) + } else { + _time = nil } } } diff --git a/macos/Classes/Types/BaseCallbackResult.swift b/macos/Classes/Types/BaseCallbackResult.swift index f7edb4a5..a76caf72 100644 --- a/macos/Classes/Types/BaseCallbackResult.swift +++ b/macos/Classes/Types/BaseCallbackResult.swift @@ -1,8 +1,8 @@ // // BaseCallbackResult.swift -// flutter_inappwebview +// shared-apple // -// Created by Lorenzo Pichilli on 06/05/22. +// Created by Lorenzo Pichilli on 17/10/22. // import Foundation diff --git a/macos/Classes/Types/CallbackResult.swift b/macos/Classes/Types/CallbackResult.swift index 5b0ba2b5..628082c0 100644 --- a/macos/Classes/Types/CallbackResult.swift +++ b/macos/Classes/Types/CallbackResult.swift @@ -1,8 +1,8 @@ // // CallbackResult.swift -// flutter_inappwebview +// shared-apple // -// Created by Lorenzo Pichilli on 06/05/22. +// Created by Lorenzo Pichilli on 17/10/22. // import Foundation diff --git a/macos/Classes/Types/MethodChannelResult.swift b/macos/Classes/Types/MethodChannelResult.swift index d1555e39..cba7eb24 100644 --- a/macos/Classes/Types/MethodChannelResult.swift +++ b/macos/Classes/Types/MethodChannelResult.swift @@ -1,8 +1,8 @@ // // MethodChannelResult.swift -// flutter_inappwebview +// shared-apple // -// Created by Lorenzo Pichilli on 06/05/22. +// Created by Lorenzo Pichilli on 17/10/22. // import Foundation diff --git a/macos/Classes/Types/NSPrinter.swift b/macos/Classes/Types/NSPrinter.swift new file mode 100644 index 00000000..58717866 --- /dev/null +++ b/macos/Classes/Types/NSPrinter.swift @@ -0,0 +1,18 @@ +// +// NSPrinter.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/10/22. +// + +import Foundation + +extension NSPrinter { + public func toMap () -> [String:Any?] { + return [ + "type": type.rawValue, + "languageLevel": languageLevel, + "name": name + ] + } +} diff --git a/macos/Classes/Util.swift b/macos/Classes/Util.swift index d248748e..762cc416 100644 --- a/macos/Classes/Util.swift +++ b/macos/Classes/Util.swift @@ -64,6 +64,21 @@ public class Util { } } + public static func getNSPrintInfoJobDisposition(name: String) -> NSPrintInfo.JobDisposition { + switch name { + case "save": + return NSPrintInfo.JobDisposition.save + case "cancel": + return NSPrintInfo.JobDisposition.cancel + case "preview": + return NSPrintInfo.JobDisposition.preview + case "spool": + return NSPrintInfo.JobDisposition.spool + default: + return NSPrintInfo.JobDisposition.spool + } + } + public static func isIPv4(address: String) -> Bool { var sin = sockaddr_in() return address.withCString({ cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) }) == 1 From 652ee52c759641c8700b4d0857b6493450498aaf Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Tue, 18 Oct 2022 18:12:33 +0200 Subject: [PATCH 3/9] updated macos docs, added getContentWidth WebView method --- CHANGELOG.md | 11 + .../in_app_browser/InAppBrowserSettings.java | 1 + .../webview/InAppWebViewInterface.java | 1 + .../webview/WebViewChannelDelegate.java | 20 + .../WebViewChannelDelegateMethods.java | 2 + .../webview/in_app_webview/InAppWebView.java | 13 + .../CHANGELOG.md | 4 + .../lib/src/exchangeable_object.dart | 2 + .../pubspec.yaml | 2 +- .../src/exchangeable_object_generator.dart | 8 + dev_packages/generators/pubspec.lock | 2 +- dev_packages/generators/pubspec.yaml | 2 +- .../in_app_browser/hide_and_show.dart | 32 + .../integration_test/in_app_browser/main.dart | 2 + .../ios/Flutter/flutter_export_environment.sh | 5 +- .../lib/in_app_browser_example.screen.dart | 1 - .../InAppBrowser/InAppBrowserManager.swift | 1 + .../InAppBrowser/InAppBrowserSettings.swift | 1 + .../InAppBrowserWebViewController.swift | 3 + ios/Classes/InAppWebView/InAppWebView.swift | 8 +- .../InAppWebView/InAppWebViewSettings.swift | 2 +- .../InAppWebView/WebViewChannelDelegate.swift | 20 +- .../WebViewChannelDelegateMethods.swift | 2 + lib/src/content_blocker.dart | 61 +- lib/src/cookie_manager.dart | 199 +- .../find_interaction_controller.dart | 8 + lib/src/http_auth_credentials_database.dart | 9 +- lib/src/in_app_browser/in_app_browser.dart | 144 +- .../in_app_browser_settings.dart | 126 +- .../in_app_browser_settings.g.dart | 259 +++ lib/src/in_app_browser/main.dart | 8 +- lib/src/in_app_localhost_server.dart | 1 + .../headless_in_app_webview.dart | 6 + .../in_app_webview_controller.dart | 212 +- .../in_app_webview_settings.dart | 1155 ++++++----- .../in_app_webview_settings.g.dart | 1705 +++++++++++++++++ lib/src/in_app_webview/main.dart | 7 +- lib/src/in_app_webview/webview.dart | 85 +- lib/src/print_job/print_job_controller.dart | 8 + lib/src/types/permission_resource_type.dart | 21 +- lib/src/types/permission_resource_type.g.dart | 12 + lib/src/types/permission_response_action.dart | 7 - .../types/permission_response_action.g.dart | 15 +- lib/src/types/ssl_error_type.dart | 30 + lib/src/types/ssl_error_type.g.dart | 18 + lib/src/types/web_resource_error_type.dart | 235 +++ lib/src/types/web_resource_error_type.g.dart | 141 ++ lib/src/web/in_app_web_view_web_element.dart | 8 +- .../web_authenticate_session.dart | 5 +- .../web_authenticate_session_settings.dart | 3 + lib/src/web_message/web_message_channel.dart | 5 + lib/src/web_message/web_message_listener.dart | 5 + lib/src/web_storage/web_storage.dart | 1 + lib/src/web_storage/web_storage_manager.dart | 4 + .../InAppBrowser/InAppBrowserManager.swift | 2 +- .../InAppBrowser/InAppBrowserSettings.swift | 16 +- .../InAppBrowserWebViewController.swift | 13 +- .../InAppBrowser/InAppBrowserWindow.swift | 3 + .../FlutterWebViewController.swift | 3 - macos/Classes/InAppWebView/InAppWebView.swift | 219 +-- .../InAppWebView/InAppWebViewSettings.swift | 29 +- .../InAppWebView/WebViewChannelDelegate.swift | 102 +- .../WebViewChannelDelegateMethods.swift | 8 +- macos/Classes/PlatformUtil.swift | 3 +- .../LastTouchedAnchorOrImageJS.swift | 62 - macos/Classes/Util.swift | 2 - pubspec.yaml | 2 +- 67 files changed, 3941 insertions(+), 1171 deletions(-) create mode 100644 example/integration_test/in_app_browser/hide_and_show.dart create mode 100644 lib/src/in_app_browser/in_app_browser_settings.g.dart create mode 100644 lib/src/in_app_webview/in_app_webview_settings.g.dart delete mode 100644 macos/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 53ed632d..6a245ce8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 6.0.0-beta.3 + +- Added MacOS support +- Added `PrintJobInfo.printer` +- Added `getContentWidth` WebView method + +### BREAKING CHANGES + +- Removed `PrintJobInfo.printerId` +- All `InAppWebViewSettings`, `InAppBrowserSettings` properties are optionals + ## 6.0.0-beta.2 - Fixed web example diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserSettings.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserSettings.java index ff7ce54d..6e720949 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserSettings.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserSettings.java @@ -95,6 +95,7 @@ public class InAppBrowserSettings implements ISettings { @Override public Map getRealSettings(@NonNull InAppBrowserActivity inAppBrowserActivity) { Map realSettings = toMap(); + realSettings.put("hidden", inAppBrowserActivity.isHidden); realSettings.put("hideToolbarTop", inAppBrowserActivity.actionBar == null || !inAppBrowserActivity.actionBar.isShowing()); realSettings.put("hideUrlBar", inAppBrowserActivity.menu == null || !inAppBrowserActivity.menu.findItem(R.id.menu_search).isVisible()); realSettings.put("hideProgressBar", inAppBrowserActivity.progressBar == null || inAppBrowserActivity.progressBar.getMax() == 0); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/InAppWebViewInterface.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/InAppWebViewInterface.java index 5c55642d..60bf917d 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/InAppWebViewInterface.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/InAppWebViewInterface.java @@ -69,6 +69,7 @@ public interface InAppWebViewInterface { String printCurrentPage(@Nullable PrintJobSettings settings); int getContentHeight(); void getContentHeight(ValueCallback callback); + void getContentWidth(ValueCallback callback); void zoomBy(float zoomFactor); String getOriginalUrl(); void getSelectedText(ValueCallback callback); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegate.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegate.java index 5dddfb13..b161a194 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegate.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegate.java @@ -263,6 +263,14 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.notImplemented(); } break; + case isHidden: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + result.success(inAppBrowserActivity.isHidden); + } else { + result.notImplemented(); + } + break; case getCopyBackForwardList: result.success((webView != null) ? webView.getCopyBackForwardList() : null); break; @@ -370,6 +378,18 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(null); } break; + case getContentWidth: + if (webView instanceof InAppWebView) { + webView.getContentWidth(new ValueCallback() { + @Override + public void onReceiveValue(@Nullable Integer contentWidth) { + result.success(contentWidth); + } + }); + } else { + result.success(null); + } + break; case zoomBy: if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { double zoomFactor = (double) call.argument("zoomFactor"); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegateMethods.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegateMethods.java index 445667bd..d87954f6 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegateMethods.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegateMethods.java @@ -27,6 +27,7 @@ public enum WebViewChannelDelegateMethods { close, show, hide, + isHidden, getCopyBackForwardList, startSafeBrowsing, clearCache, @@ -46,6 +47,7 @@ public enum WebViewChannelDelegateMethods { resumeTimers, printCurrentPage, getContentHeight, + getContentWidth, zoomBy, getOriginalUrl, getZoomScale, diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebView.java index 881dc311..b7b022cc 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebView.java @@ -1910,6 +1910,19 @@ final public class InAppWebView extends InputAwareWebView implements InAppWebVie callback.onReceiveValue(getContentHeight()); } + public void getContentWidth(final ValueCallback callback) { + evaluateJavascript("document.documentElement.scrollWidth;", new ValueCallback() { + @Override + public void onReceiveValue(@Nullable String value) { + Integer contentWidth = null; + if (value != null && !value.equalsIgnoreCase("null")) { + contentWidth = Integer.parseInt(value); + } + callback.onReceiveValue(contentWidth); + } + }); + } + @Override public void getHitTestResult(ValueCallback callback) { callback.onReceiveValue(com.pichillilorenzo.flutter_inappwebview.types.HitTestResult.fromWebViewHitTestResult(getHitTestResult())); diff --git a/dev_packages/flutter_inappwebview_internal_annotations/CHANGELOG.md b/dev_packages/flutter_inappwebview_internal_annotations/CHANGELOG.md index 35771889..a05f94be 100755 --- a/dev_packages/flutter_inappwebview_internal_annotations/CHANGELOG.md +++ b/dev_packages/flutter_inappwebview_internal_annotations/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.0 + +- Added `ExchangeableObject.copyMethod`. + ## 1.0.0 Initial release. \ No newline at end of file diff --git a/dev_packages/flutter_inappwebview_internal_annotations/lib/src/exchangeable_object.dart b/dev_packages/flutter_inappwebview_internal_annotations/lib/src/exchangeable_object.dart index 6e1a9baf..1776e8fe 100644 --- a/dev_packages/flutter_inappwebview_internal_annotations/lib/src/exchangeable_object.dart +++ b/dev_packages/flutter_inappwebview_internal_annotations/lib/src/exchangeable_object.dart @@ -4,6 +4,7 @@ class ExchangeableObject { final bool fromMapFactory; final bool nullableFromMapFactory; final bool toStringMethod; + final bool copyMethod; const ExchangeableObject({ this.toMapMethod = true, @@ -11,5 +12,6 @@ class ExchangeableObject { this.fromMapFactory = true, this.nullableFromMapFactory = true, this.toStringMethod = true, + this.copyMethod = false }); } diff --git a/dev_packages/flutter_inappwebview_internal_annotations/pubspec.yaml b/dev_packages/flutter_inappwebview_internal_annotations/pubspec.yaml index c074d9a6..01aae9a6 100755 --- a/dev_packages/flutter_inappwebview_internal_annotations/pubspec.yaml +++ b/dev_packages/flutter_inappwebview_internal_annotations/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_inappwebview_internal_annotations description: Internal annotations used by the generator of flutter_inappwebview plugin -version: 1.0.0 +version: 1.1.0 homepage: https://github.com/pichillilorenzo/flutter_inappwebview environment: diff --git a/dev_packages/generators/lib/src/exchangeable_object_generator.dart b/dev_packages/generators/lib/src/exchangeable_object_generator.dart index cbde4c7d..d0f0a8e3 100644 --- a/dev_packages/generators/lib/src/exchangeable_object_generator.dart +++ b/dev_packages/generators/lib/src/exchangeable_object_generator.dart @@ -443,6 +443,14 @@ class ExchangeableObjectGenerator classBuffer.writeln('}'); } + if (annotation.read("copyMethod").boolValue && (!visitor.methods.containsKey("copy") || + Util.methodHasIgnore(visitor.methods['copy']!))) { + classBuffer.writeln('///Returns a copy of $extClassName.'); + classBuffer.writeln('$extClassName copy() {'); + classBuffer.writeln('return $extClassName.fromMap(toMap()) ?? $extClassName();'); + classBuffer.writeln('}'); + } + if (annotation.read("toStringMethod").boolValue && (!visitor.methods.containsKey("toString") || Util.methodHasIgnore(visitor.methods['toString']!))) { classBuffer.writeln('@override'); diff --git a/dev_packages/generators/pubspec.lock b/dev_packages/generators/pubspec.lock index 3f1a113a..07f34add 100644 --- a/dev_packages/generators/pubspec.lock +++ b/dev_packages/generators/pubspec.lock @@ -187,7 +187,7 @@ packages: name: flutter_inappwebview_internal_annotations url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.0" frontend_server_client: dependency: transitive description: diff --git a/dev_packages/generators/pubspec.yaml b/dev_packages/generators/pubspec.yaml index 80b666dd..47509f8f 100755 --- a/dev_packages/generators/pubspec.yaml +++ b/dev_packages/generators/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter build: ^2.3.1 source_gen: ^1.2.5 - flutter_inappwebview_internal_annotations: ^1.0.0 + flutter_inappwebview_internal_annotations: ^1.1.0 dev_dependencies: build_runner: ^2.2.1 diff --git a/example/integration_test/in_app_browser/hide_and_show.dart b/example/integration_test/in_app_browser/hide_and_show.dart new file mode 100644 index 00000000..1e1fa155 --- /dev/null +++ b/example/integration_test/in_app_browser/hide_and_show.dart @@ -0,0 +1,32 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../constants.dart'; +import '../util.dart'; + +void hideAndShow() { + final shouldSkip = kIsWeb + ? true + : ![ + TargetPlatform.android, + TargetPlatform.iOS, + TargetPlatform.macOS, + ].contains(defaultTargetPlatform); + + test('hide and show', () async { + var inAppBrowser = new MyInAppBrowser(); + await inAppBrowser.openUrlRequest( + urlRequest: URLRequest(url: TEST_URL_1), + settings: InAppBrowserClassSettings( + browserSettings: InAppBrowserSettings(hidden: true))); + await inAppBrowser.browserCreated.future; + await inAppBrowser.firstPageLoaded.future; + + expect(await inAppBrowser.isHidden(), true); + await expectLater(inAppBrowser.show(), completes); + expect(await inAppBrowser.isHidden(), false); + await expectLater(inAppBrowser.hide(), completes); + expect(await inAppBrowser.isHidden(), true); + }, skip: shouldSkip); +} diff --git a/example/integration_test/in_app_browser/main.dart b/example/integration_test/in_app_browser/main.dart index 13fc2bd1..4b20e930 100644 --- a/example/integration_test/in_app_browser/main.dart +++ b/example/integration_test/in_app_browser/main.dart @@ -5,6 +5,7 @@ import 'open_data_and_close.dart'; import 'open_file_and_close.dart'; import 'open_url_and_close.dart'; import 'set_get_settings.dart'; +import 'hide_and_show.dart'; void main() { final shouldSkip = kIsWeb; @@ -14,5 +15,6 @@ void main() { openFileAndClose(); openDataAndClose(); setGetSettings(); + hideAndShow(); }, skip: shouldSkip); } diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index fae63896..9e98dd5e 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -3,11 +3,12 @@ export "FLUTTER_ROOT=/Users/lorenzopichilli/fvm/versions/2.10.4" export "FLUTTER_APPLICATION_PATH=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example" export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_TARGET=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" +export "DART_DEFINES=Zmx1dHRlci5pbnNwZWN0b3Iuc3RydWN0dXJlZEVycm9ycz10cnVl,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" +export "PACKAGE_CONFIG=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/.dart_tool/package_config.json" diff --git a/example/lib/in_app_browser_example.screen.dart b/example/lib/in_app_browser_example.screen.dart index 33c3a3a5..e9aa7d42 100755 --- a/example/lib/in_app_browser_example.screen.dart +++ b/example/lib/in_app_browser_example.screen.dart @@ -106,7 +106,6 @@ class _InAppBrowserExampleScreenState extends State { URLRequest(url: Uri.parse("https://flutter.dev")), settings: InAppBrowserClassSettings( browserSettings: InAppBrowserSettings( - hidden: false, toolbarTopBackgroundColor: Colors.blue, presentationStyle: ModalPresentationStyle.POPOVER ), diff --git a/ios/Classes/InAppBrowser/InAppBrowserManager.swift b/ios/Classes/InAppBrowser/InAppBrowserManager.swift index 233686be..ac621910 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserManager.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserManager.swift @@ -56,6 +56,7 @@ public class InAppBrowserManager: ChannelDelegate { let webViewController = InAppBrowserWebViewController() webViewController.browserSettings = browserSettings + webViewController.isHidden = browserSettings.hidden webViewController.webViewSettings = webViewSettings webViewController.previousStatusBarStyle = previousStatusBarStyle return webViewController diff --git a/ios/Classes/InAppBrowser/InAppBrowserSettings.swift b/ios/Classes/InAppBrowser/InAppBrowserSettings.swift index 5bd51f8f..1449b312 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserSettings.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserSettings.swift @@ -35,6 +35,7 @@ public class InAppBrowserSettings: ISettings { override func getRealSettings(obj: InAppBrowserWebViewController?) -> [String: Any?] { var realOptions: [String: Any?] = toMap() if let inAppBrowserWebViewController = obj { + realOptions["hidden"] = inAppBrowserWebViewController.isHidden realOptions["hideUrlBar"] = inAppBrowserWebViewController.searchBar.isHidden realOptions["progressBar"] = inAppBrowserWebViewController.progressBar.isHidden realOptions["closeButtonCaption"] = inAppBrowserWebViewController.closeButton.title diff --git a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift index 536eb049..ab6d8c35 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -38,6 +38,7 @@ public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelega var previousStatusBarStyle = -1 var initialUserScripts: [[String: Any]] = [] var pullToRefreshInitialSettings: [String: Any?] = [:] + var isHidden = false public override func loadView() { let channel = FlutterMethodChannel(name: InAppBrowserWebViewController.METHOD_CHANNEL_NAME_PREFIX + id, binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger()) @@ -363,6 +364,7 @@ public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelega public func show(completion: (() -> Void)? = nil) { if let navController = navigationController as? InAppBrowserNavigationController, let window = navController.tmpWindow { + isHidden = false window.alpha = 0.0 window.isHidden = false window.makeKeyAndVisible() @@ -375,6 +377,7 @@ public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelega public func hide(completion: (() -> Void)? = nil) { if let navController = navigationController as? InAppBrowserNavigationController, let window = navController.tmpWindow { + isHidden = true window.alpha = 1.0 UIView.animate(withDuration: 0.2) { window.alpha = 0.0 diff --git a/ios/Classes/InAppWebView/InAppWebView.swift b/ios/Classes/InAppWebView/InAppWebView.swift index b8c02f74..a9057d20 100755 --- a/ios/Classes/InAppWebView/InAppWebView.swift +++ b/ios/Classes/InAppWebView/InAppWebView.swift @@ -1562,8 +1562,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, } @available(iOS 15.0, *) - @available(macOS 12.0, *) - @available(macCatalyst 15.0, *) public func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, @@ -1605,8 +1603,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, } @available(iOS 15.0, *) - @available(macOS 12.0, *) - @available(macCatalyst 15.0, *) public func webView(_ webView: WKWebView, requestDeviceOrientationAndMotionPermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, @@ -2848,6 +2844,10 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { return Int64(scrollView.contentSize.height) } + public func getContentWidth() -> Int64 { + return Int64(scrollView.contentSize.width) + } + public func zoomBy(zoomFactor: Float, animated: Bool) { let currentZoomScale = scrollView.zoomScale scrollView.setZoomScale(currentZoomScale * CGFloat(zoomFactor), animated: animated) diff --git a/ios/Classes/InAppWebView/InAppWebViewSettings.swift b/ios/Classes/InAppWebView/InAppWebViewSettings.swift index c024626c..98c7dd74 100755 --- a/ios/Classes/InAppWebView/InAppWebViewSettings.swift +++ b/ios/Classes/InAppWebView/InAppWebViewSettings.swift @@ -119,7 +119,7 @@ public class InAppWebViewSettings: ISettings { } else { realSettings["mediaPlaybackRequiresUserGesture"] = configuration.mediaPlaybackRequiresUserAction } - realSettings["minimumFontSize"] = configuration.preferences.minimumFontSize + realSettings["minimumFontSize"] = Int(configuration.preferences.minimumFontSize) realSettings["suppressesIncrementalRendering"] = configuration.suppressesIncrementalRendering realSettings["allowsBackForwardNavigationGestures"] = webView.allowsBackForwardNavigationGestures realSettings["allowsInlineMediaPlayback"] = configuration.allowsInlineMediaPlayback diff --git a/ios/Classes/InAppWebView/WebViewChannelDelegate.swift b/ios/Classes/InAppWebView/WebViewChannelDelegate.swift index ed33c497..4b5e6cae 100644 --- a/ios/Classes/InAppWebView/WebViewChannelDelegate.swift +++ b/ios/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -206,6 +206,13 @@ public class WebViewChannelDelegate : ChannelDelegate { result(FlutterMethodNotImplemented) } break + case .isHidden: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + result(iabController.isHidden) + } else { + result(FlutterMethodNotImplemented) + } + break case .getCopyBackForwardList: result(webView?.getCopyBackForwardList()) break @@ -290,6 +297,9 @@ public class WebViewChannelDelegate : ChannelDelegate { case .getContentHeight: result(webView?.getContentHeight()) break + case .getContentWidth: + result(webView?.getContentWidth()) + break case .zoomBy: let zoomFactor = (arguments!["zoomFactor"] as! NSNumber).floatValue let animated = arguments!["animated"] as! Bool @@ -572,8 +582,14 @@ public class WebViewChannelDelegate : ChannelDelegate { if let webView = self.webView, #available(iOS 14.5, *) { // closeAllMediaPresentations with completionHandler v15.0 makes the app crash // with error EXC_BAD_ACCESS, so use closeAllMediaPresentations v14.5 - webView.closeAllMediaPresentations() - result(true) + if #available(iOS 16.0, *) { + webView.closeAllMediaPresentations { + result(true) + } + } else { + webView.closeAllMediaPresentations() + result(true) + } } else { result(false) } diff --git a/ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift b/ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift index 658c096f..d9e99ad1 100644 --- a/ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift +++ b/ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift @@ -34,6 +34,7 @@ public enum WebViewChannelDelegateMethods: String { case close = "close" case show = "show" case hide = "hide" + case isHidden = "isHidden" case getCopyBackForwardList = "getCopyBackForwardList" @available(*, deprecated, message: "Use FindInteractionController.findAll instead.") case findAll = "findAll" @@ -48,6 +49,7 @@ public enum WebViewChannelDelegateMethods: String { case resumeTimers = "resumeTimers" case printCurrentPage = "printCurrentPage" case getContentHeight = "getContentHeight" + case getContentWidth = "getContentWidth" case zoomBy = "zoomBy" case reloadFromOrigin = "reloadFromOrigin" case getOriginalUrl = "getOriginalUrl" diff --git a/lib/src/content_blocker.dart b/lib/src/content_blocker.dart index 9e6e448c..ae88732e 100755 --- a/lib/src/content_blocker.dart +++ b/lib/src/content_blocker.dart @@ -2,7 +2,7 @@ import 'types/main.dart'; ///Class that represents a set of rules to use block content in the browser window. /// -///On iOS, it uses [WKContentRuleListStore](https://developer.apple.com/documentation/webkit/wkcontentruleliststore). +///On iOS and MacOS, it uses [WKContentRuleListStore](https://developer.apple.com/documentation/webkit/wkcontentruleliststore). ///On Android, it uses a custom implementation because such functionality doesn't exist. /// ///In general, this [article](https://developer.apple.com/documentation/safariservices/creating_a_content_blocker) can be used to get an overview about this functionality @@ -27,6 +27,11 @@ class ContentBlocker { action: ContentBlockerAction.fromMap( Map.from(map["action"]!))); } + + @override + String toString() { + return 'ContentBlocker{trigger: $trigger, action: $action}'; + } } ///Trigger of the content blocker. The trigger tells to the WebView when to perform the corresponding action. @@ -39,40 +44,76 @@ class ContentBlockerTrigger { ///A list of regular expressions to match iframes URL against. /// - ///*NOTE*: available only on iOS. + ///**Supported Platforms/Implementations**: + ///- iOS + ///- MacOS List ifFrameUrl; ///A Boolean value. The default value is `false`. /// - ///*NOTE*: available only on iOS. + ///**Supported Platforms/Implementations**: + ///- iOS + ///- MacOS bool urlFilterIsCaseSensitive; ///A list of [ContentBlockerTriggerResourceType] representing the resource types (how the browser intends to use the resource) that the rule should match. ///If not specified, the rule matches all resource types. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS List resourceType; ///A list of strings matched to a URL's domain; limits action to a list of specific domains. ///Values must be lowercase ASCII, or punycode for non-ASCII. Add * in front to match domain and subdomains. Can't be used with [ContentBlockerTrigger.unlessDomain]. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS List ifDomain; ///A list of strings matched to a URL's domain; acts on any site except domains in a provided list. ///Values must be lowercase ASCII, or punycode for non-ASCII. Add * in front to match domain and subdomains. Can't be used with [ContentBlockerTrigger.ifDomain]. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS List unlessDomain; ///A list of [ContentBlockerTriggerLoadType] that can include one of two mutually exclusive values. If not specified, the rule matches all load types. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS List loadType; ///A list of strings matched to the entire main document URL; limits the action to a specific list of URL patterns. ///Values must be lowercase ASCII, or punycode for non-ASCII. Can't be used with [ContentBlockerTrigger.unlessTopUrl]. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS List ifTopUrl; ///An array of strings matched to the entire main document URL; acts on any site except URL patterns in provided list. ///Values must be lowercase ASCII, or punycode for non-ASCII. Can't be used with [ContentBlockerTrigger.ifTopUrl]. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS List unlessTopUrl; ///An array of strings that specify loading contexts. /// - ///*NOTE*: available only on iOS. + ///**Supported Platforms/Implementations**: + ///- iOS + ///- MacOS List loadContext; ContentBlockerTrigger( @@ -161,7 +202,7 @@ class ContentBlockerTrigger { return ContentBlockerTrigger( urlFilter: map["url-filter"], - ifFrameUrl: map["if-frame-url"], + ifFrameUrl: List.from(map["if-frame-url"] ?? []), urlFilterIsCaseSensitive: map["url-filter-is-case-sensitive"], ifDomain: List.from(map["if-domain"] ?? []), unlessDomain: List.from(map["unless-domain"] ?? []), @@ -171,6 +212,11 @@ class ContentBlockerTrigger { unlessTopUrl: List.from(map["unless-top-url"] ?? []), loadContext: loadContext); } + + @override + String toString() { + return 'ContentBlockerTrigger{urlFilter: $urlFilter, ifFrameUrl: $ifFrameUrl, urlFilterIsCaseSensitive: $urlFilterIsCaseSensitive, resourceType: $resourceType, ifDomain: $ifDomain, unlessDomain: $unlessDomain, loadType: $loadType, ifTopUrl: $ifTopUrl, unlessTopUrl: $unlessTopUrl, loadContext: $loadContext}'; + } } ///Action associated to the trigger. The action tells to the WebView what to do when the trigger is matched. @@ -213,4 +259,9 @@ class ContentBlockerAction { type: ContentBlockerActionType.fromNativeValue(map["type"])!, selector: map["selector"]); } + + @override + String toString() { + return 'ContentBlockerAction{type: $type, selector: $selector}'; + } } diff --git a/lib/src/cookie_manager.dart b/lib/src/cookie_manager.dart index f419c1a9..a97b017f 100755 --- a/lib/src/cookie_manager.dart +++ b/lib/src/cookie_manager.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'in_app_webview/in_app_webview_controller.dart'; -import 'in_app_webview/in_app_webview_settings.dart'; import 'in_app_webview/headless_in_app_webview.dart'; import 'platform_util.dart'; @@ -21,13 +20,13 @@ import 'types/main.dart'; ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS +///- MacOS ///- Web class CookieManager { static CookieManager? _instance; static const MethodChannel _channel = const MethodChannel( 'com.pichillilorenzo/flutter_inappwebview_cookiemanager'); - ///Contains only iOS-specific methods of [CookieManager]. ///Use [CookieManager] instead. @Deprecated("Use CookieManager instead") late IOSCookieManager ios; @@ -60,9 +59,9 @@ class CookieManager { ///The default value of [path] is `"/"`. /// ///[webViewController] could be used if you need to set a session-only cookie using JavaScript (so [isHttpOnly] cannot be set, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) - ///on the current URL of the [WebView] managed by that controller when you need to target iOS below 11 and Web platform. In this case the [url] parameter is ignored. + ///on the current URL of the [WebView] managed by that controller when you need to target iOS below 11, MacOS below 10.13 and Web platform. In this case the [url] parameter is ignored. /// - ///**NOTE for iOS below 11.0**: If [webViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] + ///**NOTE for iOS below 11.0 and MacOS below 10.13**: If [webViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] ///to set the cookie (session-only cookie won't work! In that case, you should set also [expiresDate] or [maxAge]). /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. @@ -72,6 +71,7 @@ class CookieManager { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - CookieManager.setCookie](https://developer.android.com/reference/android/webkit/CookieManager#setCookie(java.lang.String,%20java.lang.String,%20android.webkit.ValueCallback%3Cjava.lang.Boolean%3E))) ///- iOS ([Official API - WKHTTPCookieStore.setCookie](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882007-setcookie)) + ///- MacOS ([Official API - WKHTTPCookieStore.setCookie](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882007-setcookie)) ///- Web Future setCookie( {required Uri url, @@ -94,27 +94,19 @@ class CookieManager { assert(value.isNotEmpty); assert(path.isNotEmpty); - if (defaultTargetPlatform == TargetPlatform.iOS || kIsWeb) { - var shouldUseJavascript = kIsWeb; - if (defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb) { - var platformUtil = PlatformUtil.instance(); - var version = double.tryParse(await platformUtil.getSystemVersion()); - shouldUseJavascript = version != null && version < 11.0; - } - if (shouldUseJavascript) { - await _setCookieWithJavaScript( - url: url, - name: name, - value: value, - domain: domain, - path: path, - expiresDate: expiresDate, - maxAge: maxAge, - isSecure: isSecure, - sameSite: sameSite, - webViewController: webViewController); - return; - } + if (await _shouldUseJavascript()) { + await _setCookieWithJavaScript( + url: url, + name: name, + value: value, + domain: domain, + path: path, + expiresDate: expiresDate, + maxAge: maxAge, + isSecure: isSecure, + sameSite: sameSite, + webViewController: webViewController); + return; } Map args = {}; @@ -160,16 +152,17 @@ class CookieManager { cookieValue += ";"; if (webViewController != null) { - InAppWebViewSettings? settings = await webViewController.getSettings(); - if (settings != null && settings.javaScriptEnabled) { + final javaScriptEnabled = + (await webViewController.getSettings())?.javaScriptEnabled ?? false; + if (javaScriptEnabled) { await webViewController.evaluateJavascript( source: 'document.cookie="$cookieValue"'); return; } } - var setCookieCompleter = Completer(); - var headlessWebView = new HeadlessInAppWebView( + final setCookieCompleter = Completer(); + final headlessWebView = new HeadlessInAppWebView( initialUrlRequest: URLRequest(url: url), onLoadStop: (controller, url) async { await controller.evaluateJavascript( @@ -185,10 +178,10 @@ class CookieManager { ///Gets all the cookies for the given [url]. /// ///[webViewController] is used for getting the cookies (also session-only cookies) using JavaScript (cookies with `isHttpOnly` enabled cannot be found, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) - ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11 and Web platform. JavaScript must be enabled in order to work. + ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11, MacOS below 10.13 and Web platform. JavaScript must be enabled in order to work. ///In this case the [url] parameter is ignored. /// - ///**NOTE for iOS below 11.0**: All the cookies returned this way will have all the properties to `null` except for [Cookie.name] and [Cookie.value]. + ///**NOTE for iOS below 11.0 and MacOS below 10.13**: All the cookies returned this way will have all the properties to `null` except for [Cookie.name] and [Cookie.value]. ///If [webViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] ///to get the cookies (session-only cookies and cookies with `isHttpOnly` enabled won't be found!). /// @@ -199,6 +192,7 @@ class CookieManager { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - CookieManager.getCookie](https://developer.android.com/reference/android/webkit/CookieManager#getCookie(java.lang.String))) ///- iOS ([Official API - WKHTTPCookieStore.getAllCookies](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882005-getallcookies)) + ///- MacOS ([Official API - WKHTTPCookieStore.getAllCookies](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882005-getallcookies)) ///- Web Future> getCookies( {required Uri url, @@ -209,17 +203,9 @@ class CookieManager { webViewController = webViewController ?? iosBelow11WebViewController; - if (defaultTargetPlatform == TargetPlatform.iOS || kIsWeb) { - var shouldUseJavascript = kIsWeb; - if (defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb) { - var platformUtil = PlatformUtil.instance(); - var version = double.tryParse(await platformUtil.getSystemVersion()); - shouldUseJavascript = version != null && version < 11.0; - } - if (shouldUseJavascript) { - return await _getCookiesWithJavaScript( - url: url, webViewController: webViewController); - } + if (await _shouldUseJavascript()) { + return await _getCookiesWithJavaScript( + url: url, webViewController: webViewController); } List cookies = []; @@ -253,8 +239,9 @@ class CookieManager { List cookies = []; if (webViewController != null) { - InAppWebViewSettings? settings = await webViewController.getSettings(); - if (settings != null && settings.javaScriptEnabled) { + final javaScriptEnabled = + (await webViewController.getSettings())?.javaScriptEnabled ?? false; + if (javaScriptEnabled) { List documentCookies = (await webViewController .evaluateJavascript(source: 'document.cookie') as String) .split(';') @@ -273,8 +260,8 @@ class CookieManager { } } - var pageLoaded = Completer(); - var headlessWebView = new HeadlessInAppWebView( + final pageLoaded = Completer(); + final headlessWebView = new HeadlessInAppWebView( initialUrlRequest: URLRequest(url: url), onLoadStop: (controller, url) async { pageLoaded.complete(); @@ -304,10 +291,10 @@ class CookieManager { ///Gets a cookie by its [name] for the given [url]. /// ///[webViewController] is used for getting the cookie (also session-only cookie) using JavaScript (cookie with `isHttpOnly` enabled cannot be found, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) - ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11 and Web platform. JavaScript must be enabled in order to work. + ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11, MacOS below 10.13 and Web platform. JavaScript must be enabled in order to work. ///In this case the [url] parameter is ignored. /// - ///**NOTE for iOS below 11.0**: All the cookies returned this way will have all the properties to `null` except for [Cookie.name] and [Cookie.value]. + ///**NOTE for iOS below 11.0 and MacOS below 10.13**: All the cookies returned this way will have all the properties to `null` except for [Cookie.name] and [Cookie.value]. ///If [webViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] ///to get the cookie (session-only cookie and cookie with `isHttpOnly` enabled won't be found!). /// @@ -318,6 +305,7 @@ class CookieManager { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future getCookie( {required Uri url, @@ -330,20 +318,12 @@ class CookieManager { webViewController = webViewController ?? iosBelow11WebViewController; - if (defaultTargetPlatform == TargetPlatform.iOS || kIsWeb) { - var shouldUseJavascript = kIsWeb; - if (defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb) { - var platformUtil = PlatformUtil.instance(); - var version = double.tryParse(await platformUtil.getSystemVersion()); - shouldUseJavascript = version != null && version < 11.0; - } - if (shouldUseJavascript) { - List cookies = await _getCookiesWithJavaScript( - url: url, webViewController: webViewController); - return cookies - .cast() - .firstWhere((cookie) => cookie!.name == name, orElse: () => null); - } + if (await _shouldUseJavascript()) { + List cookies = await _getCookiesWithJavaScript( + url: url, webViewController: webViewController); + return cookies + .cast() + .firstWhere((cookie) => cookie!.name == name, orElse: () => null); } Map args = {}; @@ -373,10 +353,10 @@ class CookieManager { ///The default value of [path] is `"/"`. /// ///[webViewController] is used for deleting the cookie (also session-only cookie) using JavaScript (cookie with `isHttpOnly` enabled cannot be deleted, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) - ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11 and Web platform. JavaScript must be enabled in order to work. + ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11, MacOS below 10.13 and Web platform. JavaScript must be enabled in order to work. ///In this case the [url] parameter is ignored. /// - ///**NOTE for iOS below 11.0**: If [webViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] + ///**NOTE for iOS below 11.0 and MacOS below 10.13**: If [webViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] ///to delete the cookie (session-only cookie and cookie with `isHttpOnly` enabled won't be deleted!). /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. @@ -386,6 +366,7 @@ class CookieManager { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - WKHTTPCookieStore.delete](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882009-delete) + ///- MacOS ([Official API - WKHTTPCookieStore.delete](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882009-delete) ///- Web Future deleteCookie( {required Uri url, @@ -400,24 +381,16 @@ class CookieManager { webViewController = webViewController ?? iosBelow11WebViewController; - if (defaultTargetPlatform == TargetPlatform.iOS || kIsWeb) { - var shouldUseJavascript = kIsWeb; - if (defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb) { - var platformUtil = PlatformUtil.instance(); - var version = double.tryParse(await platformUtil.getSystemVersion()); - shouldUseJavascript = version != null && version < 11.0; - } - if (shouldUseJavascript) { - await _setCookieWithJavaScript( - url: url, - name: name, - value: "", - path: path, - domain: domain, - maxAge: -1, - webViewController: webViewController); - return; - } + if (await _shouldUseJavascript()) { + await _setCookieWithJavaScript( + url: url, + name: name, + value: "", + path: path, + domain: domain, + maxAge: -1, + webViewController: webViewController); + return; } Map args = {}; @@ -433,10 +406,10 @@ class CookieManager { ///The default value of [path] is `"/"`. /// ///[webViewController] is used for deleting the cookies (also session-only cookies) using JavaScript (cookies with `isHttpOnly` enabled cannot be deleted, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) - ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11 and Web platform. JavaScript must be enabled in order to work. + ///from the current context of the [WebView] managed by that controller when you need to target iOS below 11, MacOS below 10.13 and Web platform. JavaScript must be enabled in order to work. ///In this case the [url] parameter is ignored. /// - ///**NOTE for iOS below 11.0**: If [webViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] + ///**NOTE for iOS below 11.0 and MacOS below 10.13**: If [webViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] ///to delete the cookies (session-only cookies and cookies with `isHttpOnly` enabled won't be deleted!). /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. @@ -446,6 +419,7 @@ class CookieManager { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future deleteCookies( {required Uri url, @@ -458,28 +432,20 @@ class CookieManager { webViewController = webViewController ?? iosBelow11WebViewController; - if (defaultTargetPlatform == TargetPlatform.iOS || kIsWeb) { - var shouldUseJavascript = kIsWeb; - if (defaultTargetPlatform == TargetPlatform.iOS && !kIsWeb) { - var platformUtil = PlatformUtil.instance(); - var version = double.tryParse(await platformUtil.getSystemVersion()); - shouldUseJavascript = version != null && version < 11.0; - } - if (shouldUseJavascript) { - List cookies = await _getCookiesWithJavaScript( - url: url, webViewController: webViewController); - for (var i = 0; i < cookies.length; i++) { - await _setCookieWithJavaScript( - url: url, - name: cookies[i].name, - value: "", - path: path, - domain: domain, - maxAge: -1, - webViewController: webViewController); - } - return; + if (await _shouldUseJavascript()) { + List cookies = await _getCookiesWithJavaScript( + url: url, webViewController: webViewController); + for (var i = 0; i < cookies.length; i++) { + await _setCookieWithJavaScript( + url: url, + name: cookies[i].name, + value: "", + path: path, + domain: domain, + maxAge: -1, + webViewController: webViewController); } + return; } Map args = {}; @@ -493,9 +459,12 @@ class CookieManager { /// ///**NOTE for iOS**: available from iOS 11.0+. /// + ///**NOTE for MacOS**: available from iOS 10.13+. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - CookieManager.removeAllCookies](https://developer.android.com/reference/android/webkit/CookieManager#removeAllCookies(android.webkit.ValueCallback%3Cjava.lang.Boolean%3E))) ///- iOS ([Official API - WKWebsiteDataStore.removeData](https://developer.apple.com/documentation/webkit/wkwebsitedatastore/1532938-removedata)) + ///- MacOS ([Official API - WKWebsiteDataStore.removeData](https://developer.apple.com/documentation/webkit/wkwebsitedatastore/1532938-removedata)) Future deleteAllCookies() async { Map args = {}; await _channel.invokeMethod('deleteAllCookies', args); @@ -503,10 +472,13 @@ class CookieManager { ///Fetches all stored cookies. /// - ///**NOTE**: available on iOS 11.0+. + ///**NOTE for iOS**: available on iOS 11.0+. + /// + ///**NOTE for MacOS**: available from iOS 10.13+. /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKHTTPCookieStore.getAllCookies](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882005-getallcookies)) + ///- MacOS ([Official API - WKHTTPCookieStore.getAllCookies](https://developer.apple.com/documentation/webkit/wkhttpcookiestore/2882005-getallcookies)) Future> getAllCookies() async { List cookies = []; @@ -542,6 +514,21 @@ class CookieManager { timezone: 'GMT') : await platformUtil.getWebCookieExpirationDate(date: dateTime); } + + Future _shouldUseJavascript() async { + if (kIsWeb) { + return true; + } + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + final platformUtil = PlatformUtil.instance(); + final systemVersion = await platformUtil.getSystemVersion(); + return defaultTargetPlatform == TargetPlatform.iOS + ? systemVersion.compareTo("11") == -1 + : systemVersion.compareTo("10.13") == -1; + } + return false; + } } ///Class that contains only iOS-specific methods of [CookieManager]. diff --git a/lib/src/find_interaction/find_interaction_controller.dart b/lib/src/find_interaction/find_interaction_controller.dart index b63fe7a5..d6a24d3c 100644 --- a/lib/src/find_interaction/find_interaction_controller.dart +++ b/lib/src/find_interaction/find_interaction_controller.dart @@ -9,6 +9,7 @@ import '../types/main.dart'; ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS +///- MacOS class FindInteractionController { MethodChannel? _channel; @@ -29,6 +30,7 @@ class FindInteractionController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.FindListener.onFindResultReceived](https://developer.android.com/reference/android/webkit/WebView.FindListener#onFindResultReceived(int,%20int,%20boolean))) ///- iOS + ///- MacOS final void Function( FindInteractionController controller, int activeMatchOrdinal, @@ -108,6 +110,7 @@ class FindInteractionController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.findAllAsync](https://developer.android.com/reference/android/webkit/WebView#findAllAsync(java.lang.String))) ///- iOS (if [InAppWebViewSettings.isFindInteractionEnabled] is `true`: [Official API - UIFindInteraction.presentFindNavigator](https://developer.apple.com/documentation/uikit/uifindinteraction/3975832-presentfindnavigator?changes=_2) with [Official API - UIFindInteraction.searchText](https://developer.apple.com/documentation/uikit/uifindinteraction/3975834-searchtext?changes=_2)) + ///- MacOS Future findAll({String? find}) async { Map args = {}; args.putIfAbsent('find', () => find); @@ -125,6 +128,7 @@ class FindInteractionController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.findNext](https://developer.android.com/reference/android/webkit/WebView#findNext(boolean))) ///- iOS (if [InAppWebViewSettings.isFindInteractionEnabled] is `true`: [Official API - UIFindInteraction.findNext](https://developer.apple.com/documentation/uikit/uifindinteraction/3975829-findnext?changes=_2) and ([Official API - UIFindInteraction.findPrevious](https://developer.apple.com/documentation/uikit/uifindinteraction/3975830-findprevious?changes=_2))) + ///- MacOS Future findNext({bool forward = true}) async { Map args = {}; args.putIfAbsent('forward', () => forward); @@ -140,6 +144,7 @@ class FindInteractionController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.clearMatches](https://developer.android.com/reference/android/webkit/WebView#clearMatches())) ///- iOS (if [InAppWebViewSettings.isFindInteractionEnabled] is `true`: [Official API - UIFindInteraction.dismissFindNavigator](https://developer.apple.com/documentation/uikit/uifindinteraction/3975827-dismissfindnavigator?changes=_2)) + ///- MacOS Future clearMatches() async { Map args = {}; await _channel?.invokeMethod('clearMatches', args); @@ -153,6 +158,7 @@ class FindInteractionController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - UIFindInteraction.searchText](https://developer.apple.com/documentation/uikit/uifindinteraction/3975834-searchtext?changes=_2)) + ///- MacOS Future setSearchText(String? searchText) async { Map args = {}; args.putIfAbsent('searchText', () => searchText); @@ -167,6 +173,7 @@ class FindInteractionController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - UIFindInteraction.searchText](https://developer.apple.com/documentation/uikit/uifindinteraction/3975834-searchtext?changes=_2)) + ///- MacOS Future getSearchText() async { Map args = {}; return await _channel?.invokeMethod('getSearchText', args); @@ -221,6 +228,7 @@ class FindInteractionController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - UIFindInteraction.activeFindSession](https://developer.apple.com/documentation/uikit/uifindinteraction/3975825-activefindsession?changes=_7____4_8&language=objc)) + ///- MacOS Future getActiveFindSession() async { Map args = {}; Map? result = diff --git a/lib/src/http_auth_credentials_database.dart b/lib/src/http_auth_credentials_database.dart index 2650eb53..53027398 100755 --- a/lib/src/http_auth_credentials_database.dart +++ b/lib/src/http_auth_credentials_database.dart @@ -4,7 +4,7 @@ import 'types/main.dart'; import 'package:flutter/services.dart'; ///Class that implements a singleton object (shared instance) which manages the shared HTTP auth credentials cache. -///On iOS, this class uses the [URLCredentialStorage](https://developer.apple.com/documentation/foundation/urlcredentialstorage) class. +///On iOS and MacOS, this class uses the [URLCredentialStorage](https://developer.apple.com/documentation/foundation/urlcredentialstorage) class. ///On Android, this class has a custom implementation using `android.database.sqlite.SQLiteDatabase` because ///[WebViewDatabase](https://developer.android.com/reference/android/webkit/WebViewDatabase) ///doesn't offer the same functionalities as iOS `URLCredentialStorage`. @@ -12,6 +12,7 @@ import 'package:flutter/services.dart'; ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS +///- MacOS class HttpAuthCredentialDatabase { static HttpAuthCredentialDatabase? _instance; static const MethodChannel _channel = const MethodChannel( @@ -44,6 +45,7 @@ class HttpAuthCredentialDatabase { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - URLCredentialStorage.allCredentials](https://developer.apple.com/documentation/foundation/urlcredentialstorage/1413859-allcredentials)) + ///- MacOS ([Official API - URLCredentialStorage.allCredentials](https://developer.apple.com/documentation/foundation/urlcredentialstorage/1413859-allcredentials)) Future> getAllAuthCredentials() async { Map args = {}; @@ -67,6 +69,7 @@ class HttpAuthCredentialDatabase { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future> getHttpAuthCredentials( {required URLProtectionSpace protectionSpace}) async { Map args = {}; @@ -91,6 +94,7 @@ class HttpAuthCredentialDatabase { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - URLCredentialStorage.set](https://developer.apple.com/documentation/foundation/urlcredentialstorage/1407227-set)) + ///- MacOS ([Official API - URLCredentialStorage.set](https://developer.apple.com/documentation/foundation/urlcredentialstorage/1407227-set)) Future setHttpAuthCredential( {required URLProtectionSpace protectionSpace, required URLCredential credential}) async { @@ -109,6 +113,7 @@ class HttpAuthCredentialDatabase { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - URLCredentialStorage.remove](https://developer.apple.com/documentation/foundation/urlcredentialstorage/1408664-remove)) + ///- MacOS ([Official API - URLCredentialStorage.remove](https://developer.apple.com/documentation/foundation/urlcredentialstorage/1408664-remove)) Future removeHttpAuthCredential( {required URLProtectionSpace protectionSpace, required URLCredential credential}) async { @@ -127,6 +132,7 @@ class HttpAuthCredentialDatabase { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future removeHttpAuthCredentials( {required URLProtectionSpace protectionSpace}) async { Map args = {}; @@ -142,6 +148,7 @@ class HttpAuthCredentialDatabase { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future clearAllAuthCredentials() async { Map args = {}; await _channel.invokeMethod('clearAllAuthCredentials', args); diff --git a/lib/src/in_app_browser/in_app_browser.dart b/lib/src/in_app_browser/in_app_browser.dart index 8084ed82..a02ac2ed 100755 --- a/lib/src/in_app_browser/in_app_browser.dart +++ b/lib/src/in_app_browser/in_app_browser.dart @@ -48,6 +48,7 @@ class InAppBrowserNotOpenedException implements Exception { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS +///- MacOS class InAppBrowser { ///Debug settings. static DebugLoggingSettings debugLoggingSettings = DebugLoggingSettings(); @@ -152,6 +153,11 @@ class InAppBrowser { ///[options]: Options for the [InAppBrowser]. /// ///[settings]: Settings for the [InAppBrowser]. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS Future openUrlRequest( {required URLRequest urlRequest, // ignore: deprecated_member_use_from_same_package @@ -220,6 +226,11 @@ class InAppBrowser { ///[options]: Options for the [InAppBrowser]. /// ///[settings]: Settings for the [InAppBrowser]. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS Future openFile( {required String assetFilePath, // ignore: deprecated_member_use_from_same_package @@ -262,6 +273,11 @@ class InAppBrowser { ///The [options] parameter specifies the options for the [InAppBrowser]. /// ///[settings]: Settings for the [InAppBrowser]. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS Future openData( {required String data, String mimeType = "text/html", @@ -303,6 +319,11 @@ class InAppBrowser { } ///This is a static method that opens an [url] in the system browser. You wont be able to use the [InAppBrowser] methods here! + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS static Future openWithSystemBrowser({required Uri url}) async { assert(url.toString().isNotEmpty); Map args = {}; @@ -311,6 +332,11 @@ class InAppBrowser { } ///Displays an [InAppBrowser] window that was opened hidden. Calling this has no effect if the [InAppBrowser] was already visible. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS Future show() async { this.throwIfNotOpened(); Map args = {}; @@ -318,6 +344,11 @@ class InAppBrowser { } ///Hides the [InAppBrowser] window. Calling this has no effect if the [InAppBrowser] was already hidden. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS Future hide() async { this.throwIfNotOpened(); Map args = {}; @@ -325,6 +356,11 @@ class InAppBrowser { } ///Closes the [InAppBrowser] window. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS Future close() async { this.throwIfNotOpened(); Map args = {}; @@ -332,6 +368,11 @@ class InAppBrowser { } ///Check if the Web View of the [InAppBrowser] instance is hidden. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS Future isHidden() async { this.throwIfNotOpened(); Map args = {}; @@ -365,6 +406,11 @@ class InAppBrowser { } ///Sets the [InAppBrowser] settings with the new [settings] and evaluates them. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS Future setSettings( {required InAppBrowserClassSettings settings}) async { this.throwIfNotOpened(); @@ -375,6 +421,11 @@ class InAppBrowser { } ///Gets the current [InAppBrowser] settings. Returns `null` if it wasn't able to get them. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS Future getSettings() async { this.throwIfNotOpened(); Map args = {}; @@ -391,14 +442,29 @@ class InAppBrowser { } ///Returns `true` if the [InAppBrowser] instance is opened, otherwise `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS bool isOpened() { return this._isOpened; } ///Event fired when the [InAppBrowser] is created. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS void onBrowserCreated() {} ///Event fired when the [InAppBrowser] window is closed. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS void onExit() {} ///Event fired when the [InAppBrowser] starts to load an [url]. @@ -406,6 +472,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.onPageStarted](https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455621-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455621-webview)) void onLoadStart(Uri? url) {} ///Event fired when the [InAppBrowser] finishes loading an [url]. @@ -413,6 +480,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.onPageFinished](https://developer.android.com/reference/android/webkit/WebViewClient#onPageFinished(android.webkit.WebView,%20java.lang.String))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455629-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455629-webview)) void onLoadStop(Uri? url) {} ///Use [onReceivedError] instead. @@ -424,6 +492,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.onReceivedError](https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedError(android.webkit.WebView,%20android.webkit.WebResourceRequest,%20android.webkit.WebResourceError))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455623-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455623-webview)) void onReceivedError(WebResourceRequest request, WebResourceError error) {} ///Use [onReceivedHttpError] instead. @@ -441,6 +510,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.onReceivedHttpError](https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedHttpError(android.webkit.WebView,%20android.webkit.WebResourceRequest,%20android.webkit.WebResourceResponse))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview)) void onReceivedHttpError( WebResourceRequest request, WebResourceResponse errorResponse) {} @@ -449,6 +519,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onProgressChanged](https://developer.android.com/reference/android/webkit/WebChromeClient#onProgressChanged(android.webkit.WebView,%20int))) ///- iOS + ///- MacOS void onProgressChanged(int progress) {} ///Event fired when the [InAppBrowser] webview receives a [ConsoleMessage]. @@ -456,23 +527,25 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onConsoleMessage](https://developer.android.com/reference/android/webkit/WebChromeClient#onConsoleMessage(android.webkit.ConsoleMessage))) ///- iOS + ///- MacOS void onConsoleMessage(ConsoleMessage consoleMessage) {} ///Give the host application a chance to take control when a URL is about to be loaded in the current WebView. This event is not called on the initial load of the WebView. /// ///Note that on Android there isn't any way to load an URL for a frame that is not the main frame, so if the request is not for the main frame, the navigation is allowed by default. - ///However, if you want to cancel requests for subframes, you can use the [InAppWebViewSettings.regexToCancelSubFramesLoading] option + ///However, if you want to cancel requests for subframes, you can use the [InAppWebViewSettings.regexToCancelSubFramesLoading] setting ///to write a Regular Expression that, if the url request of a subframe matches, then the request of that subframe is canceled. /// ///Also, on Android, this method is not called for POST requests. /// ///[navigationAction] represents an object that contains information about an action that causes navigation to occur. /// - ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useShouldOverrideUrlLoading] option to `true`. + ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useShouldOverrideUrlLoading] setting to `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.shouldOverrideUrlLoading](https://developer.android.com/reference/android/webkit/WebViewClient#shouldOverrideUrlLoading(android.webkit.WebView,%20java.lang.String))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455641-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455641-webview)) Future? shouldOverrideUrlLoading( NavigationAction navigationAction) { return null; @@ -480,11 +553,12 @@ class InAppBrowser { ///Event fired when the [InAppBrowser] webview loads a resource. /// - ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useOnLoadResource] and [InAppWebViewSettings.javaScriptEnabled] options to `true`. + ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useOnLoadResource] and [InAppWebViewSettings.javaScriptEnabled] setting to `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS void onLoadResource(LoadedResource resource) {} ///Event fired when the [InAppBrowser] webview scrolls. @@ -493,9 +567,12 @@ class InAppBrowser { /// ///[y] represents the current vertical scroll origin in pixels. /// + ///**NOTE for MacOS**: this method is implemented with using JavaScript. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.onScrollChanged](https://developer.android.com/reference/android/webkit/WebView#onScrollChanged(int,%20int,%20int,%20int))) ///- iOS ([Official API - UIScrollViewDelegate.scrollViewDidScroll](https://developer.apple.com/documentation/uikit/uiscrollviewdelegate/1619392-scrollviewdidscroll)) + ///- MacOS void onScrollChanged(int x, int y) {} ///Use [onDownloadStartRequest] instead @@ -507,11 +584,12 @@ class InAppBrowser { /// ///[downloadStartRequest] represents the request of the file to download. /// - ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useOnDownloadStart] option to `true`. + ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useOnDownloadStart] setting to `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.setDownloadListener](https://developer.android.com/reference/android/webkit/WebView#setDownloadListener(android.webkit.DownloadListener))) ///- iOS + ///- MacOS void onDownloadStartRequest(DownloadStartRequest downloadStartRequest) {} ///Use [onLoadResourceWithCustomScheme] instead. @@ -526,6 +604,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - WKURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkurlschemehandler)) + ///- MacOS ([Official API - WKURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkurlschemehandler)) Future? onLoadResourceWithCustomScheme( WebResourceRequest request) { return null; @@ -539,11 +618,11 @@ class InAppBrowser { /// ///[createWindowAction] represents the request. /// - ///**NOTE**: to allow JavaScript to open windows, you need to set [InAppWebViewSettings.javaScriptCanOpenWindowsAutomatically] option to `true`. + ///**NOTE**: to allow JavaScript to open windows, you need to set [InAppWebViewSettings.javaScriptCanOpenWindowsAutomatically] setting to `true`. /// - ///**NOTE**: on Android you need to set [InAppWebViewSettings.supportMultipleWindows] option to `true`. + ///**NOTE**: on Android you need to set [InAppWebViewSettings.supportMultipleWindows] setting to `true`. /// - ///**NOTE**: on iOS, setting these initial options: [InAppWebViewSettings.supportZoom], [InAppWebViewSettings.useOnLoadResource], [InAppWebViewSettings.useShouldInterceptAjaxRequest], + ///**NOTE**: on iOS and MacOS, setting these initial settings: [InAppWebViewSettings.supportZoom], [InAppWebViewSettings.useOnLoadResource], [InAppWebViewSettings.useShouldInterceptAjaxRequest], ///[InAppWebViewSettings.useShouldInterceptFetchRequest], [InAppWebViewSettings.applicationNameForUserAgent], [InAppWebViewSettings.javaScriptCanOpenWindowsAutomatically], ///[InAppWebViewSettings.javaScriptEnabled], [InAppWebViewSettings.minimumFontSize], [InAppWebViewSettings.preferredContentMode], [InAppWebViewSettings.incognito], ///[InAppWebViewSettings.cacheEnabled], [InAppWebViewSettings.mediaPlaybackRequiresUserGesture], @@ -562,6 +641,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onCreateWindow](https://developer.android.com/reference/android/webkit/WebChromeClient#onCreateWindow(android.webkit.WebView,%20boolean,%20boolean,%20android.os.Message))) ///- iOS ([Official API - WKUIDelegate.webView](https://developer.apple.com/documentation/webkit/wkuidelegate/1536907-webview)) + ///- MacOS ([Official API - WKUIDelegate.webView](https://developer.apple.com/documentation/webkit/wkuidelegate/1536907-webview)) Future? onCreateWindow(CreateWindowAction createWindowAction) { return null; } @@ -572,6 +652,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onCloseWindow](https://developer.android.com/reference/android/webkit/WebChromeClient#onCloseWindow(android.webkit.WebView))) ///- iOS ([Official API - WKUIDelegate.webViewDidClose](https://developer.apple.com/documentation/webkit/wkuidelegate/1537390-webviewdidclose)) + ///- MacOS ([Official API - WKUIDelegate.webViewDidClose](https://developer.apple.com/documentation/webkit/wkuidelegate/1537390-webviewdidclose)) void onCloseWindow() {} ///Event fired when the JavaScript `window` object of the WebView has received focus. @@ -580,6 +661,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS void onWindowFocus() {} ///Event fired when the JavaScript `window` object of the WebView has lost focus. @@ -588,6 +670,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS void onWindowBlur() {} ///Event fired when javascript calls the `alert()` method to display an alert dialog. @@ -598,6 +681,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onJsAlert](https://developer.android.com/reference/android/webkit/WebChromeClient#onJsAlert(android.webkit.WebView,%20java.lang.String,%20java.lang.String,%20android.webkit.JsResult))) ///- iOS ([Official API - WKUIDelegate.webView](https://developer.apple.com/documentation/webkit/wkuidelegate/1537406-webview)) + ///- MacOS ([Official API - WKUIDelegate.webView](https://developer.apple.com/documentation/webkit/wkuidelegate/1537406-webview)) Future? onJsAlert(JsAlertRequest jsAlertRequest) { return null; } @@ -610,6 +694,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onJsConfirm](https://developer.android.com/reference/android/webkit/WebChromeClient#onJsConfirm(android.webkit.WebView,%20java.lang.String,%20java.lang.String,%20android.webkit.JsResult))) ///- iOS ([Official API - WKUIDelegate.webView](https://developer.apple.com/documentation/webkit/wkuidelegate/1536489-webview)) + ///- MacOS ([Official API - WKUIDelegate.webView](https://developer.apple.com/documentation/webkit/wkuidelegate/1536489-webview)) Future? onJsConfirm(JsConfirmRequest jsConfirmRequest) { return null; } @@ -622,6 +707,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onJsPrompt](https://developer.android.com/reference/android/webkit/WebChromeClient#onJsPrompt(android.webkit.WebView,%20java.lang.String,%20java.lang.String,%20java.lang.String,%20android.webkit.JsPromptResult))) ///- iOS ([Official API - WKUIDelegate.webView](https://developer.apple.com/documentation/webkit/wkuidelegate/1538086-webview)) + ///- MacOS ([Official API - WKUIDelegate.webView](https://developer.apple.com/documentation/webkit/wkuidelegate/1538086-webview)) Future? onJsPrompt(JsPromptRequest jsPromptRequest) { return null; } @@ -633,6 +719,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.onReceivedHttpAuthRequest](https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedHttpAuthRequest(android.webkit.WebView,%20android.webkit.HttpAuthHandler,%20java.lang.String,%20java.lang.String))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455638-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455638-webview)) Future? onReceivedHttpAuthRequest( URLAuthenticationChallenge challenge) { return null; @@ -646,6 +733,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.onReceivedSslError](https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedSslError(android.webkit.WebView,%20android.webkit.SslErrorHandler,%20android.net.http.SslError))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455638-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455638-webview)) Future? onReceivedServerTrustAuthRequest( URLAuthenticationChallenge challenge) { return null; @@ -661,6 +749,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.onReceivedClientCertRequest](https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedClientCertRequest(android.webkit.WebView,%20android.webkit.ClientCertRequest))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455638-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455638-webview)) Future? onReceivedClientCertRequest( URLAuthenticationChallenge challenge) { return null; @@ -676,7 +765,7 @@ class InAppBrowser { /// ///[ajaxRequest] represents the `XMLHttpRequest`. /// - ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useShouldInterceptAjaxRequest] option to `true`. + ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useShouldInterceptAjaxRequest] setting to `true`. ///Also, unlike iOS that has [WKUserScript](https://developer.apple.com/documentation/webkit/wkuserscript) that ///can inject javascript code right after the document element is created but before any other content is loaded, in Android the javascript code ///used to intercept ajax requests is loaded as soon as possible so it won't be instantaneous as iOS but just after some milliseconds (< ~100ms). @@ -685,6 +774,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future? shouldInterceptAjaxRequest(AjaxRequest ajaxRequest) { return null; } @@ -694,7 +784,7 @@ class InAppBrowser { /// ///[ajaxRequest] represents the [XMLHttpRequest]. /// - ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useShouldInterceptAjaxRequest] option to `true`. + ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useShouldInterceptAjaxRequest] setting to `true`. ///Also, unlike iOS that has [WKUserScript](https://developer.apple.com/documentation/webkit/wkuserscript) that ///can inject javascript code right after the document element is created but before any other content is loaded, in Android the javascript code ///used to intercept ajax requests is loaded as soon as possible so it won't be instantaneous as iOS but just after some milliseconds (< ~100ms). @@ -703,6 +793,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future? onAjaxReadyStateChange(AjaxRequest ajaxRequest) { return null; } @@ -712,7 +803,7 @@ class InAppBrowser { /// ///[ajaxRequest] represents the [XMLHttpRequest]. /// - ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useShouldInterceptAjaxRequest] option to `true`. + ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useShouldInterceptAjaxRequest] setting to `true`. ///Also, unlike iOS that has [WKUserScript](https://developer.apple.com/documentation/webkit/wkuserscript) that ///can inject javascript code right after the document element is created but before any other content is loaded, in Android the javascript code ///used to intercept ajax requests is loaded as soon as possible so it won't be instantaneous as iOS but just after some milliseconds (< ~100ms). @@ -721,6 +812,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future? onAjaxProgress(AjaxRequest ajaxRequest) { return null; } @@ -730,7 +822,7 @@ class InAppBrowser { /// ///[fetchRequest] represents a resource request. /// - ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useShouldInterceptFetchRequest] option to `true`. + ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useShouldInterceptFetchRequest] setting to `true`. ///Also, unlike iOS that has [WKUserScript](https://developer.apple.com/documentation/webkit/wkuserscript) that ///can inject javascript code right after the document element is created but before any other content is loaded, in Android the javascript code ///used to intercept fetch requests is loaded as soon as possible so it won't be instantaneous as iOS but just after some milliseconds (< ~100ms). @@ -739,6 +831,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future? shouldInterceptFetchRequest( FetchRequest fetchRequest) { return null; @@ -756,6 +849,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.doUpdateVisitedHistory](https://developer.android.com/reference/android/webkit/WebViewClient#doUpdateVisitedHistory(android.webkit.WebView,%20java.lang.String,%20boolean))) ///- iOS + ///- MacOS void onUpdateVisitedHistory(Uri? url, bool? isReload) {} ///Use [onPrintRequest] instead @@ -773,6 +867,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future? onPrintRequest( Uri? url, PrintJobController? printJobController) { return null; @@ -792,6 +887,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onShowCustomView](https://developer.android.com/reference/android/webkit/WebChromeClient#onShowCustomView(android.view.View,%20android.webkit.WebChromeClient.CustomViewCallback))) ///- iOS ([Official API - UIWindow.didBecomeVisibleNotification](https://developer.apple.com/documentation/uikit/uiwindow/1621621-didbecomevisiblenotification)) + ///- MacOS ([Official API - NSWindow.didEnterFullScreenNotification](https://developer.apple.com/documentation/appkit/nswindow/1419651-didenterfullscreennotification)) void onEnterFullscreen() {} ///Event fired when the current page has exited full screen mode. @@ -799,6 +895,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onHideCustomView](https://developer.android.com/reference/android/webkit/WebChromeClient#onHideCustomView())) ///- iOS ([Official API - UIWindow.didBecomeHiddenNotification](https://developer.apple.com/documentation/uikit/uiwindow/1621617-didbecomehiddennotification)) + ///- MacOS ([Official API - NSWindow.didExitFullScreenNotification](https://developer.apple.com/documentation/appkit/nswindow/1419177-didexitfullscreennotification)) void onExitFullscreen() {} ///Called when the web view begins to receive web content. @@ -811,6 +908,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewClient.onPageCommitVisible](https://developer.android.com/reference/android/webkit/WebViewClient#onPageCommitVisible(android.webkit.WebView,%20java.lang.String))) ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455635-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455635-webview)) void onPageCommitVisible(Uri? url) {} ///Event fired when a change in the document title occurred. @@ -820,6 +918,7 @@ class InAppBrowser { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onReceivedTitle](https://developer.android.com/reference/android/webkit/WebChromeClient#onReceivedTitle(android.webkit.WebView,%20java.lang.String))) ///- iOS + ///- MacOS void onTitleChanged(String? title) {} ///Event fired to respond to the results of an over-scroll operation. @@ -888,9 +987,12 @@ class InAppBrowser { /// ///**NOTE for iOS**: available only on iOS 15.0+. The default [PermissionResponse.action] is [PermissionResponseAction.PROMPT]. /// + ///**NOTE for MacOS**: available only on iOS 12.0+. The default [PermissionResponse.action] is [PermissionResponseAction.PROMPT]. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebChromeClient.onPermissionRequest](https://developer.android.com/reference/android/webkit/WebChromeClient#onPermissionRequest(android.webkit.PermissionRequest))) ///- iOS + ///- MacOS Future? onPermissionRequest( PermissionRequest permissionRequest) { return null; @@ -1112,6 +1214,7 @@ class InAppBrowser { /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKNavigationDelegate.webViewWebContentProcessDidTerminate](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455639-webviewwebcontentprocessdidtermi)) + ///- MacOS ([Official API - WKNavigationDelegate.webViewWebContentProcessDidTerminate](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455639-webviewwebcontentprocessdidtermi)) void onWebContentProcessDidTerminate() {} ///Use [onDidReceiveServerRedirectForProvisionalNavigation] instead. @@ -1122,6 +1225,7 @@ class InAppBrowser { /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455627-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455627-webview)) void onDidReceiveServerRedirectForProvisionalNavigation() {} ///Use [onNavigationResponse] instead. @@ -1135,10 +1239,11 @@ class InAppBrowser { /// ///[navigationResponse] represents the navigation response. /// - ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useOnNavigationResponse] option to `true`. + ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewSettings.useOnNavigationResponse] setting to `true`. /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview)) Future? onNavigationResponse( NavigationResponse navigationResponse) { return null; @@ -1155,10 +1260,13 @@ class InAppBrowser { /// ///[challenge] represents the authentication challenge. /// - ///**NOTE**: available only on iOS 14.0+. + ///**NOTE for iOS**: available only on iOS 14.0+. + /// + ///**NOTE for MacOS**: available only on MacOS 11.0+. /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/3601237-webview)) + ///- MacOS ([Official API - WKNavigationDelegate.webView](https://developer.apple.com/documentation/webkit/wknavigationdelegate/3601237-webview)) Future? shouldAllowDeprecatedTLS( URLAuthenticationChallenge challenge) { return null; @@ -1166,10 +1274,13 @@ class InAppBrowser { ///Event fired when a change in the camera capture state occurred. /// - ///**NOTE**: available only on iOS 15.0+. + ///**NOTE for iOS**: available only on iOS 15.0+. + /// + ///**NOTE for MacOS**: available only on MacOS 12.0+. /// ///**Supported Platforms/Implementations**: ///- iOS + ///- MacOS void onCameraCaptureStateChanged( MediaCaptureState? oldState, MediaCaptureState? newState, @@ -1177,10 +1288,13 @@ class InAppBrowser { ///Event fired when a change in the microphone capture state occurred. /// - ///**NOTE**: available only on iOS 15.0+. + ///**NOTE for iOS**: available only on iOS 15.0+. + /// + ///**NOTE for MacOS**: available only on MacOS 12.0+. /// ///**Supported Platforms/Implementations**: ///- iOS + ///- MacOS void onMicrophoneCaptureStateChanged( MediaCaptureState? oldState, MediaCaptureState? newState, diff --git a/lib/src/in_app_browser/in_app_browser_settings.dart b/lib/src/in_app_browser/in_app_browser_settings.dart index cd4e66d4..f0bfc15f 100755 --- a/lib/src/in_app_browser/in_app_browser_settings.dart +++ b/lib/src/in_app_browser/in_app_browser_settings.dart @@ -2,7 +2,10 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview/src/types/main.dart'; +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; +import '../types/modal_presentation_style.dart'; +import '../types/modal_transition_style.dart'; import '../util.dart'; import '../in_app_webview/in_app_webview_settings.dart'; @@ -13,6 +16,8 @@ import '../in_app_webview/android/in_app_webview_options.dart'; import 'apple/in_app_browser_options.dart'; import '../in_app_webview/apple/in_app_webview_options.dart'; +part 'in_app_browser_settings.g.dart'; + ///Class that represents the settings that can be used for an [InAppBrowser] instance. class InAppBrowserClassSettings { ///Browser settings. @@ -50,8 +55,8 @@ class InAppBrowserClassSettings { if (instance == null) { instance = InAppBrowserClassSettings(); } - instance.browserSettings = InAppBrowserSettings.fromMap(options); - instance.webViewSettings = InAppWebViewSettings.fromMap(options); + instance.browserSettings = InAppBrowserSettings.fromMap(options) ?? InAppBrowserSettings(); + instance.webViewSettings = InAppWebViewSettings.fromMap(options) ?? InAppWebViewSettings(); return instance; } @@ -84,7 +89,8 @@ class BrowserOptions { } ///This class represents all [InAppBrowser] settings available. -class InAppBrowserSettings +@ExchangeableObject(copyMethod: true) +class InAppBrowserSettings_ implements BrowserOptions, AndroidOptions, IosOptions { ///Set to `true` to create the browser and load the page, but not show it. Omit or set to `false` to have the browser open and load normally. ///The default value is `false`. @@ -92,20 +98,23 @@ class InAppBrowserSettings ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool hidden; + ///- MacOS + bool? hidden; ///Set to `true` to hide the toolbar at the top of the WebView. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool hideToolbarTop; + ///- MacOS + bool? hideToolbarTop; ///Set the custom background color of the toolbar at the top. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Color? toolbarTopBackgroundColor; ///Set to `true` to hide the url bar on the toolbar at the top. The default value is `false`. @@ -113,50 +122,53 @@ class InAppBrowserSettings ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool hideUrlBar; + ///- MacOS + bool? hideUrlBar; ///Set to `true` to hide the progress bar when the WebView is loading a page. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool hideProgressBar; + ///- MacOS + bool? hideProgressBar; ///Set to `true` if you want the title should be displayed. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool hideTitleBar; + bool? hideTitleBar; ///Set the action bar's title. /// ///**Supported Platforms/Implementations**: ///- Android native WebView + ///- MacOS String? toolbarTopFixedTitle; ///Set to `false` to not close the InAppBrowser when the user click on the Android back button and the WebView cannot go back to the history. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool closeOnCannotGoBack; + bool? closeOnCannotGoBack; ///Set to `false` to block the InAppBrowser WebView going back when the user click on the Android back button. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool allowGoBackWithBackButton; + bool? allowGoBackWithBackButton; ///Set to `true` to close the InAppBrowser when the user click on the Android back button. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool shouldCloseOnBackButtonPressed; + bool? shouldCloseOnBackButtonPressed; ///Set to `true` to set the toolbar at the top translucent. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- iOS - bool toolbarTopTranslucent; + bool? toolbarTopTranslucent; ///Set the tint color to apply to the navigation bar background. /// @@ -174,7 +186,7 @@ class InAppBrowserSettings /// ///**Supported Platforms/Implementations**: ///- iOS - bool hideToolbarBottom; + bool? hideToolbarBottom; ///Set the custom background color of the toolbar at the bottom. /// @@ -192,7 +204,7 @@ class InAppBrowserSettings /// ///**Supported Platforms/Implementations**: ///- iOS - bool toolbarBottomTranslucent; + bool? toolbarBottomTranslucent; ///Set the custom text for the close button. /// @@ -210,15 +222,15 @@ class InAppBrowserSettings /// ///**Supported Platforms/Implementations**: ///- iOS - ModalPresentationStyle presentationStyle; + ModalPresentationStyle_? presentationStyle; ///Set to the custom transition style when presenting the WebView. The default value is [ModalTransitionStyle.COVER_VERTICAL]. /// ///**Supported Platforms/Implementations**: ///- iOS - ModalTransitionStyle transitionStyle; + ModalTransitionStyle_? transitionStyle; - InAppBrowserSettings( + InAppBrowserSettings_( {this.hidden = false, this.hideToolbarTop = false, this.toolbarTopBackgroundColor, @@ -232,8 +244,8 @@ class InAppBrowserSettings this.toolbarBottomTranslucent = true, this.closeButtonCaption, this.closeButtonColor, - this.presentationStyle = ModalPresentationStyle.FULL_SCREEN, - this.transitionStyle = ModalTransitionStyle.COVER_VERTICAL, + this.presentationStyle = ModalPresentationStyle_.FULL_SCREEN, + this.transitionStyle = ModalTransitionStyle_.COVER_VERTICAL, this.hideTitleBar = false, this.toolbarTopFixedTitle, this.closeOnCannotGoBack = true, @@ -241,81 +253,21 @@ class InAppBrowserSettings this.shouldCloseOnBackButtonPressed = false}); @override - Map toMap() { - return { - "hidden": hidden, - "hideToolbarTop": hideToolbarTop, - "toolbarTopBackgroundColor": toolbarTopBackgroundColor?.toHex(), - "hideUrlBar": hideUrlBar, - "hideProgressBar": hideProgressBar, - "hideTitleBar": hideTitleBar, - "toolbarTopFixedTitle": toolbarTopFixedTitle, - "closeOnCannotGoBack": closeOnCannotGoBack, - "allowGoBackWithBackButton": allowGoBackWithBackButton, - "shouldCloseOnBackButtonPressed": shouldCloseOnBackButtonPressed, - "toolbarTopTranslucent": toolbarTopTranslucent, - "toolbarTopTintColor": toolbarTopTintColor?.toHex(), - "hideToolbarBottom": hideToolbarBottom, - "toolbarBottomBackgroundColor": toolbarBottomBackgroundColor?.toHex(), - "toolbarBottomTintColor": toolbarBottomTintColor?.toHex(), - "toolbarBottomTranslucent": toolbarBottomTranslucent, - "closeButtonCaption": closeButtonCaption, - "closeButtonColor": closeButtonColor?.toHex(), - "presentationStyle": presentationStyle.toNativeValue(), - "transitionStyle": transitionStyle.toNativeValue(), - }; - } - - static InAppBrowserSettings fromMap(Map map) { - var settings = InAppBrowserSettings(); - settings.hidden = map["hidden"]; - settings.hideToolbarTop = map["hideToolbarTop"]; - settings.toolbarTopBackgroundColor = - UtilColor.fromHex(map["toolbarTopBackgroundColor"]); - settings.hideUrlBar = map["hideUrlBar"]; - settings.hideProgressBar = map["hideProgressBar"]; - if (defaultTargetPlatform == TargetPlatform.android) { - settings.hideTitleBar = map["hideTitleBar"]; - settings.toolbarTopFixedTitle = map["toolbarTopFixedTitle"]; - settings.closeOnCannotGoBack = map["closeOnCannotGoBack"]; - settings.allowGoBackWithBackButton = map["allowGoBackWithBackButton"]; - settings.shouldCloseOnBackButtonPressed = - map["shouldCloseOnBackButtonPressed"]; - } - if (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS) { - settings.toolbarTopTranslucent = map["toolbarTopTranslucent"]; - settings.toolbarTopTintColor = - UtilColor.fromHex(map["toolbarTopTintColor"]); - settings.hideToolbarBottom = map["hideToolbarBottom"]; - settings.toolbarBottomBackgroundColor = - UtilColor.fromHex(map["toolbarBottomBackgroundColor"]); - settings.toolbarBottomTintColor = - UtilColor.fromHex(map["toolbarBottomTintColor"]); - settings.toolbarBottomTranslucent = map["toolbarBottomTranslucent"]; - settings.closeButtonCaption = map["closeButtonCaption"]; - settings.closeButtonColor = UtilColor.fromHex(map["closeButtonColor"]); - settings.presentationStyle = - ModalPresentationStyle.fromNativeValue(map["presentationStyle"])!; - settings.transitionStyle = - ModalTransitionStyle.fromNativeValue(map["transitionStyle"])!; - } - return settings; + @ExchangeableObjectMethod(ignore: true) + InAppBrowserSettings_ copy() { + throw UnimplementedError(); } @override + @ExchangeableObjectMethod(ignore: true) Map toJson() { - return this.toMap(); + throw UnimplementedError(); } @override - String toString() { - return toMap().toString(); - } - - @override - InAppBrowserSettings copy() { - return InAppBrowserSettings.fromMap(this.toMap()); + @ExchangeableObjectMethod(ignore: true) + Map toMap() { + throw UnimplementedError(); } } diff --git a/lib/src/in_app_browser/in_app_browser_settings.g.dart b/lib/src/in_app_browser/in_app_browser_settings.g.dart new file mode 100644 index 00000000..7d89a162 --- /dev/null +++ b/lib/src/in_app_browser/in_app_browser_settings.g.dart @@ -0,0 +1,259 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'in_app_browser_settings.dart'; + +// ************************************************************************** +// ExchangeableObjectGenerator +// ************************************************************************** + +///This class represents all [InAppBrowser] settings available. +class InAppBrowserSettings + implements BrowserOptions, AndroidOptions, IosOptions { + ///Set to `true` to create the browser and load the page, but not show it. Omit or set to `false` to have the browser open and load normally. + ///The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS + bool? hidden; + + ///Set to `true` to hide the toolbar at the top of the WebView. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS + bool? hideToolbarTop; + + ///Set the custom background color of the toolbar at the top. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS + Color? toolbarTopBackgroundColor; + + ///Set to `true` to hide the url bar on the toolbar at the top. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS + bool? hideUrlBar; + + ///Set to `true` to hide the progress bar when the WebView is loading a page. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS + ///- MacOS + bool? hideProgressBar; + + ///Set to `true` if you want the title should be displayed. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + bool? hideTitleBar; + + ///Set the action bar's title. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- MacOS + String? toolbarTopFixedTitle; + + ///Set to `false` to not close the InAppBrowser when the user click on the Android back button and the WebView cannot go back to the history. The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + bool? closeOnCannotGoBack; + + ///Set to `false` to block the InAppBrowser WebView going back when the user click on the Android back button. The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + bool? allowGoBackWithBackButton; + + ///Set to `true` to close the InAppBrowser when the user click on the Android back button. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + bool? shouldCloseOnBackButtonPressed; + + ///Set to `true` to set the toolbar at the top translucent. The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + bool? toolbarTopTranslucent; + + ///Set the tint color to apply to the navigation bar background. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + Color? toolbarTopBarTintColor; + + ///Set the tint color to apply to the navigation items and bar button items. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + Color? toolbarTopTintColor; + + ///Set to `true` to hide the toolbar at the bottom of the WebView. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + bool? hideToolbarBottom; + + ///Set the custom background color of the toolbar at the bottom. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + Color? toolbarBottomBackgroundColor; + + ///Set the tint color to apply to the bar button items. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + Color? toolbarBottomTintColor; + + ///Set to `true` to set the toolbar at the bottom translucent. The default value is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + bool? toolbarBottomTranslucent; + + ///Set the custom text for the close button. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + String? closeButtonCaption; + + ///Set the custom color for the close button. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + Color? closeButtonColor; + + ///Set the custom modal presentation style when presenting the WebView. The default value is [ModalPresentationStyle.FULL_SCREEN]. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + ModalPresentationStyle? presentationStyle; + + ///Set to the custom transition style when presenting the WebView. The default value is [ModalTransitionStyle.COVER_VERTICAL]. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + ModalTransitionStyle? transitionStyle; + InAppBrowserSettings( + {this.hidden = false, + this.hideToolbarTop = false, + this.toolbarTopBackgroundColor, + this.hideUrlBar = false, + this.hideProgressBar = false, + this.hideTitleBar = false, + this.toolbarTopFixedTitle, + this.closeOnCannotGoBack = true, + this.allowGoBackWithBackButton = true, + this.shouldCloseOnBackButtonPressed = false, + this.toolbarTopTranslucent = true, + this.toolbarTopTintColor, + this.hideToolbarBottom = false, + this.toolbarBottomBackgroundColor, + this.toolbarBottomTintColor, + this.toolbarBottomTranslucent = true, + this.closeButtonCaption, + this.closeButtonColor, + this.presentationStyle = ModalPresentationStyle.FULL_SCREEN, + this.transitionStyle = ModalTransitionStyle.COVER_VERTICAL}); + + ///Gets a possible [InAppBrowserSettings] instance from a [Map] value. + static InAppBrowserSettings? fromMap(Map? map) { + if (map == null) { + return null; + } + final instance = InAppBrowserSettings( + toolbarTopBackgroundColor: map['toolbarTopBackgroundColor'] != null + ? UtilColor.fromStringRepresentation(map['toolbarTopBackgroundColor']) + : null, + toolbarTopFixedTitle: map['toolbarTopFixedTitle'], + toolbarTopTintColor: map['toolbarTopTintColor'] != null + ? UtilColor.fromStringRepresentation(map['toolbarTopTintColor']) + : null, + toolbarBottomBackgroundColor: map['toolbarBottomBackgroundColor'] != null + ? UtilColor.fromStringRepresentation( + map['toolbarBottomBackgroundColor']) + : null, + toolbarBottomTintColor: map['toolbarBottomTintColor'] != null + ? UtilColor.fromStringRepresentation(map['toolbarBottomTintColor']) + : null, + closeButtonCaption: map['closeButtonCaption'], + closeButtonColor: map['closeButtonColor'] != null + ? UtilColor.fromStringRepresentation(map['closeButtonColor']) + : null, + ); + instance.hidden = map['hidden']; + instance.hideToolbarTop = map['hideToolbarTop']; + instance.hideUrlBar = map['hideUrlBar']; + instance.hideProgressBar = map['hideProgressBar']; + instance.hideTitleBar = map['hideTitleBar']; + instance.closeOnCannotGoBack = map['closeOnCannotGoBack']; + instance.allowGoBackWithBackButton = map['allowGoBackWithBackButton']; + instance.shouldCloseOnBackButtonPressed = + map['shouldCloseOnBackButtonPressed']; + instance.toolbarTopTranslucent = map['toolbarTopTranslucent']; + instance.toolbarTopBarTintColor = map['toolbarTopBarTintColor'] != null + ? UtilColor.fromStringRepresentation(map['toolbarTopBarTintColor']) + : null; + instance.hideToolbarBottom = map['hideToolbarBottom']; + instance.toolbarBottomTranslucent = map['toolbarBottomTranslucent']; + instance.presentationStyle = + ModalPresentationStyle.fromNativeValue(map['presentationStyle']); + instance.transitionStyle = + ModalTransitionStyle.fromNativeValue(map['transitionStyle']); + return instance; + } + + ///Converts instance to a map. + Map toMap() { + return { + "hidden": hidden, + "hideToolbarTop": hideToolbarTop, + "toolbarTopBackgroundColor": toolbarTopBackgroundColor?.toHex(), + "hideUrlBar": hideUrlBar, + "hideProgressBar": hideProgressBar, + "hideTitleBar": hideTitleBar, + "toolbarTopFixedTitle": toolbarTopFixedTitle, + "closeOnCannotGoBack": closeOnCannotGoBack, + "allowGoBackWithBackButton": allowGoBackWithBackButton, + "shouldCloseOnBackButtonPressed": shouldCloseOnBackButtonPressed, + "toolbarTopTranslucent": toolbarTopTranslucent, + "toolbarTopBarTintColor": toolbarTopBarTintColor?.toHex(), + "toolbarTopTintColor": toolbarTopTintColor?.toHex(), + "hideToolbarBottom": hideToolbarBottom, + "toolbarBottomBackgroundColor": toolbarBottomBackgroundColor?.toHex(), + "toolbarBottomTintColor": toolbarBottomTintColor?.toHex(), + "toolbarBottomTranslucent": toolbarBottomTranslucent, + "closeButtonCaption": closeButtonCaption, + "closeButtonColor": closeButtonColor?.toHex(), + "presentationStyle": presentationStyle?.toNativeValue(), + "transitionStyle": transitionStyle?.toNativeValue(), + }; + } + + ///Converts instance to a map. + Map toJson() { + return toMap(); + } + + ///Returns a copy of InAppBrowserSettings. + InAppBrowserSettings copy() { + return InAppBrowserSettings.fromMap(toMap()) ?? InAppBrowserSettings(); + } + + @override + String toString() { + return 'InAppBrowserSettings{hidden: $hidden, hideToolbarTop: $hideToolbarTop, toolbarTopBackgroundColor: $toolbarTopBackgroundColor, hideUrlBar: $hideUrlBar, hideProgressBar: $hideProgressBar, hideTitleBar: $hideTitleBar, toolbarTopFixedTitle: $toolbarTopFixedTitle, closeOnCannotGoBack: $closeOnCannotGoBack, allowGoBackWithBackButton: $allowGoBackWithBackButton, shouldCloseOnBackButtonPressed: $shouldCloseOnBackButtonPressed, toolbarTopTranslucent: $toolbarTopTranslucent, toolbarTopBarTintColor: $toolbarTopBarTintColor, toolbarTopTintColor: $toolbarTopTintColor, hideToolbarBottom: $hideToolbarBottom, toolbarBottomBackgroundColor: $toolbarBottomBackgroundColor, toolbarBottomTintColor: $toolbarBottomTintColor, toolbarBottomTranslucent: $toolbarBottomTranslucent, closeButtonCaption: $closeButtonCaption, closeButtonColor: $closeButtonColor, presentationStyle: $presentationStyle, transitionStyle: $transitionStyle}'; + } +} diff --git a/lib/src/in_app_browser/main.dart b/lib/src/in_app_browser/main.dart index 069ebf66..0c541535 100644 --- a/lib/src/in_app_browser/main.dart +++ b/lib/src/in_app_browser/main.dart @@ -1,4 +1,10 @@ export 'in_app_browser.dart'; -export 'in_app_browser_settings.dart'; +export 'in_app_browser_settings.dart' + show + InAppBrowserClassSettings, + BrowserOptions, + InAppBrowserSettings, + InAppBrowserClassOptions, + InAppBrowserOptions; export 'android/main.dart'; export 'apple/main.dart'; diff --git a/lib/src/in_app_localhost_server.dart b/lib/src/in_app_localhost_server.dart index 57c74e49..452ddbb1 100755 --- a/lib/src/in_app_localhost_server.dart +++ b/lib/src/in_app_localhost_server.dart @@ -12,6 +12,7 @@ import 'mime_type_resolver.dart'; ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS +///- MacOS class InAppLocalhostServer { bool _started = false; HttpServer? _server; diff --git a/lib/src/in_app_webview/headless_in_app_webview.dart b/lib/src/in_app_webview/headless_in_app_webview.dart index 291b43b5..36400aeb 100644 --- a/lib/src/in_app_webview/headless_in_app_webview.dart +++ b/lib/src/in_app_webview/headless_in_app_webview.dart @@ -23,6 +23,7 @@ import '../types/disposable.dart'; ///- Android native WebView ///- iOS ///- Web +///- MacOS class HeadlessInAppWebView implements WebView, Disposable { ///View ID. late final String id; @@ -192,6 +193,7 @@ class HeadlessInAppWebView implements WebView, Disposable { ///- Android native WebView ///- iOS ///- Web + ///- MacOS Future run() async { if (_started) { return; @@ -236,6 +238,7 @@ class HeadlessInAppWebView implements WebView, Disposable { ///- Android native WebView ///- iOS ///- Web + ///- MacOS @override Future dispose() async { if (!_running) { @@ -253,6 +256,7 @@ class HeadlessInAppWebView implements WebView, Disposable { ///- Android native WebView ///- iOS ///- Web + ///- MacOS bool isRunning() { return _running; } @@ -270,6 +274,7 @@ class HeadlessInAppWebView implements WebView, Disposable { ///- Android native WebView ///- iOS ///- Web + ///- MacOS Future setSize(Size size) async { if (!_running) { return; @@ -288,6 +293,7 @@ class HeadlessInAppWebView implements WebView, Disposable { ///- Android native WebView ///- iOS ///- Web + ///- MacOS Future getSize() async { if (!_running) { return null; diff --git a/lib/src/in_app_webview/in_app_webview_controller.dart b/lib/src/in_app_webview/in_app_webview_controller.dart index eafba17b..214fbf2d 100644 --- a/lib/src/in_app_webview/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/in_app_webview_controller.dart @@ -1343,6 +1343,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.getUrl](https://developer.android.com/reference/android/webkit/WebView#getUrl())) ///- iOS ([Official API - WKWebView.url](https://developer.apple.com/documentation/webkit/wkwebview/1415005-url)) + ///- MacOS ([Official API - WKWebView.url](https://developer.apple.com/documentation/webkit/wkwebview/1415005-url)) ///- Web Future getUrl() async { Map args = {}; @@ -1357,6 +1358,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.getTitle](https://developer.android.com/reference/android/webkit/WebView#getTitle())) ///- iOS ([Official API - WKWebView.title](https://developer.apple.com/documentation/webkit/wkwebview/1415015-title)) + ///- MacOS ([Official API - WKWebView.title](https://developer.apple.com/documentation/webkit/wkwebview/1415015-title)) ///- Web Future getTitle() async { Map args = {}; @@ -1368,6 +1370,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.getProgress](https://developer.android.com/reference/android/webkit/WebView#getProgress())) ///- iOS ([Official API - WKWebView.estimatedProgress](https://developer.apple.com/documentation/webkit/wkwebview/1415007-estimatedprogress)) + ///- MacOS ([Official API - WKWebView.estimatedProgress](https://developer.apple.com/documentation/webkit/wkwebview/1415007-estimatedprogress)) Future getProgress() async { Map args = {}; return await _channel.invokeMethod('getProgress', args); @@ -1383,6 +1386,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future getHtml() async { String? html; @@ -1427,6 +1431,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future> getFavicons() async { List favicons = []; @@ -1611,6 +1616,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.loadUrl](https://developer.android.com/reference/android/webkit/WebView#loadUrl(java.lang.String))). If method is "POST", [Official API - WebView.postUrl](https://developer.android.com/reference/android/webkit/WebView#postUrl(java.lang.String,%20byte[])) ///- iOS ([Official API - WKWebView.load](https://developer.apple.com/documentation/webkit/wkwebview/1414954-load). If [allowingReadAccessTo] is used, [Official API - WKWebView.loadFileURL](https://developer.apple.com/documentation/webkit/wkwebview/1414973-loadfileurl)) + ///- MacOS ([Official API - WKWebView.load](https://developer.apple.com/documentation/webkit/wkwebview/1414954-load). If [allowingReadAccessTo] is used, [Official API - WKWebView.loadFileURL](https://developer.apple.com/documentation/webkit/wkwebview/1414973-loadfileurl)) ///- Web Future loadUrl( {required URLRequest urlRequest, @@ -1646,6 +1652,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.postUrl](https://developer.android.com/reference/android/webkit/WebView#postUrl(java.lang.String,%20byte[]))) ///- iOS + ///- MacOS ///- Web Future postUrl({required Uri url, required Uint8List postData}) async { assert(url.toString().isNotEmpty); @@ -1672,6 +1679,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.loadDataWithBaseURL](https://developer.android.com/reference/android/webkit/WebView#loadDataWithBaseURL(java.lang.String,%20java.lang.String,%20java.lang.String,%20java.lang.String,%20java.lang.String))) ///- iOS ([Official API - WKWebView.loadHTMLString](https://developer.apple.com/documentation/webkit/wkwebview/1415004-loadhtmlstring) or [Official API - WKWebView.load](https://developer.apple.com/documentation/webkit/wkwebview/1415011-load)) + ///- MacOS ([Official API - WKWebView.loadHTMLString](https://developer.apple.com/documentation/webkit/wkwebview/1415004-loadhtmlstring) or [Official API - WKWebView.load](https://developer.apple.com/documentation/webkit/wkwebview/1415011-load)) ///- Web Future loadData( {required String data, @@ -1741,6 +1749,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.loadUrl](https://developer.android.com/reference/android/webkit/WebView#loadUrl(java.lang.String))) ///- iOS ([Official API - WKWebView.load](https://developer.apple.com/documentation/webkit/wkwebview/1414954-load)) + ///- MacOS ([Official API - WKWebView.load](https://developer.apple.com/documentation/webkit/wkwebview/1414954-load)) ///- Web Future loadFile({required String assetFilePath}) async { assert(assetFilePath.isNotEmpty); @@ -1756,6 +1765,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.reload](https://developer.android.com/reference/android/webkit/WebView#reload())) ///- iOS ([Official API - WKWebView.reload](https://developer.apple.com/documentation/webkit/wkwebview/1414969-reload)) + ///- MacOS ([Official API - WKWebView.reload](https://developer.apple.com/documentation/webkit/wkwebview/1414969-reload)) ///- Web ([Official API - Location.reload](https://developer.mozilla.org/en-US/docs/Web/API/Location/reload)) Future reload() async { Map args = {}; @@ -1769,6 +1779,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.goBack](https://developer.android.com/reference/android/webkit/WebView#goBack())) ///- iOS ([Official API - WKWebView.goBack](https://developer.apple.com/documentation/webkit/wkwebview/1414952-goback)) + ///- MacOS ([Official API - WKWebView.goBack](https://developer.apple.com/documentation/webkit/wkwebview/1414952-goback)) ///- Web ([Official API - History.back](https://developer.mozilla.org/en-US/docs/Web/API/History/back)) Future goBack() async { Map args = {}; @@ -1780,6 +1791,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.canGoBack](https://developer.android.com/reference/android/webkit/WebView#canGoBack())) ///- iOS ([Official API - WKWebView.canGoBack](https://developer.apple.com/documentation/webkit/wkwebview/1414966-cangoback)) + ///- MacOS ([Official API - WKWebView.canGoBack](https://developer.apple.com/documentation/webkit/wkwebview/1414966-cangoback)) Future canGoBack() async { Map args = {}; return await _channel.invokeMethod('canGoBack', args); @@ -1792,6 +1804,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.goForward](https://developer.android.com/reference/android/webkit/WebView#goForward())) ///- iOS ([Official API - WKWebView.goForward](https://developer.apple.com/documentation/webkit/wkwebview/1414993-goforward)) + ///- MacOS ([Official API - WKWebView.goForward](https://developer.apple.com/documentation/webkit/wkwebview/1414993-goforward)) ///- Web ([Official API - History.forward](https://developer.mozilla.org/en-US/docs/Web/API/History/forward)) Future goForward() async { Map args = {}; @@ -1803,6 +1816,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.canGoForward](https://developer.android.com/reference/android/webkit/WebView#canGoForward())) ///- iOS ([Official API - WKWebView.canGoForward](https://developer.apple.com/documentation/webkit/wkwebview/1414962-cangoforward)) + ///- MacOS ([Official API - WKWebView.canGoForward](https://developer.apple.com/documentation/webkit/wkwebview/1414962-cangoforward)) Future canGoForward() async { Map args = {}; return await _channel.invokeMethod('canGoForward', args); @@ -1815,6 +1829,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.goBackOrForward](https://developer.android.com/reference/android/webkit/WebView#goBackOrForward(int))) ///- iOS ([Official API - WKWebView.go](https://developer.apple.com/documentation/webkit/wkwebview/1414991-go)) + ///- MacOS ([Official API - WKWebView.go](https://developer.apple.com/documentation/webkit/wkwebview/1414991-go)) ///- Web ([Official API - History.go](https://developer.mozilla.org/en-US/docs/Web/API/History/go)) Future goBackOrForward({required int steps}) async { Map args = {}; @@ -1827,6 +1842,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.canGoBackOrForward](https://developer.android.com/reference/android/webkit/WebView#canGoBackOrForward(int))) ///- iOS + ///- MacOS Future canGoBackOrForward({required int steps}) async { Map args = {}; args.putIfAbsent('steps', () => steps); @@ -1840,6 +1856,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future goTo({required WebHistoryItem historyItem}) async { var steps = historyItem.offset; @@ -1853,6 +1870,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future isLoading() async { Map args = {}; @@ -1866,6 +1884,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.stopLoading](https://developer.android.com/reference/android/webkit/WebView#stopLoading())) ///- iOS ([Official API - WKWebView.stopLoading](https://developer.apple.com/documentation/webkit/wkwebview/1414981-stoploading)) + ///- MacOS ([Official API - WKWebView.stopLoading](https://developer.apple.com/documentation/webkit/wkwebview/1414981-stoploading)) ///- Web ([Official API - Window.stop](https://developer.mozilla.org/en-US/docs/Web/API/Window/stop)) Future stopLoading() async { Map args = {}; @@ -1879,7 +1898,7 @@ class InAppWebViewController { ///This parameter doesn’t apply to changes you make to the underlying web content, such as the document’s DOM structure. ///Those changes remain visible to all scripts, regardless of which content world you specify. ///For more information about content worlds, see [ContentWorld]. - ///Available on iOS 14.0+. + ///Available on iOS 14.0+ and MacOS 11.0+. ///**NOTE**: not used on Web. /// ///**NOTE**: This method shouldn't be called in the [WebView.onWebViewCreated] or [WebView.onLoadStart] events, @@ -1892,6 +1911,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.evaluateJavascript](https://developer.android.com/reference/android/webkit/WebView#evaluateJavascript(java.lang.String,%20android.webkit.ValueCallback%3Cjava.lang.String%3E))) ///- iOS ([Official API - WKWebView.evaluateJavascript](https://developer.apple.com/documentation/webkit/wkwebview/3656442-evaluatejavascript)) + ///- MacOS ([Official API - WKWebView.evaluateJavascript](https://developer.apple.com/documentation/webkit/wkwebview/3656442-evaluatejavascript)) ///- Web ([Official API - Window.eval](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval?retiredLocale=it)) Future evaluateJavascript( {required String source, ContentWorld? contentWorld}) async { @@ -1924,6 +1944,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future injectJavascriptFileFromUrl( {required Uri urlFile, @@ -1952,6 +1973,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future injectJavascriptFileFromAsset( {required String assetFilePath}) async { @@ -1971,6 +1993,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future injectCSSCode({required String source}) async { Map args = {}; @@ -1992,6 +2015,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future injectCSSFileFromUrl( {required Uri urlFile, @@ -2016,6 +2040,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future injectCSSFileFromAsset({required String assetFilePath}) async { String source = await rootBundle.loadString(assetFilePath); @@ -2076,6 +2101,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS void addJavaScriptHandler( {required String handlerName, required JavaScriptHandlerCallback callback}) { @@ -2091,6 +2117,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS JavaScriptHandlerCallback? removeJavaScriptHandler( {required String handlerName}) { return this.javaScriptHandlersMap.remove(handlerName); @@ -2102,9 +2129,12 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available on iOS 11.0+. /// + ///**NOTE for MacOS**: available on MacOS 10.13+. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - WKWebView.takeSnapshot](https://developer.apple.com/documentation/webkit/wkwebview/2873260-takesnapshot)) + ///- MacOS ([Official API - WKWebView.takeSnapshot](https://developer.apple.com/documentation/webkit/wkwebview/2873260-takesnapshot)) Future takeScreenshot( {ScreenshotConfiguration? screenshotConfiguration}) async { Map args = {}; @@ -2117,7 +2147,7 @@ class InAppWebViewController { @Deprecated('Use setSettings instead') Future setOptions({required InAppWebViewGroupOptions options}) async { InAppWebViewSettings settings = - InAppWebViewSettings.fromMap(options.toMap()); + InAppWebViewSettings.fromMap(options.toMap()) ?? InAppWebViewSettings(); await setSettings(settings: settings); } @@ -2140,6 +2170,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future setSettings({required InAppWebViewSettings settings}) async { Map args = {}; @@ -2153,6 +2184,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future getSettings() async { Map args = {}; @@ -2175,6 +2207,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.copyBackForwardList](https://developer.android.com/reference/android/webkit/WebView#copyBackForwardList())) ///- iOS ([Official API - WKWebView.backForwardList](https://developer.apple.com/documentation/webkit/wkwebview/1414977-backforwardlist)) + ///- MacOS ([Official API - WKWebView.backForwardList](https://developer.apple.com/documentation/webkit/wkwebview/1414977-backforwardlist)) Future getCopyBackForwardList() async { Map args = {}; Map? result = @@ -2188,6 +2221,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future clearCache() async { Map args = {}; await _channel.invokeMethod('clearCache', args); @@ -2238,9 +2272,12 @@ class InAppWebViewController { /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. /// + ///**NOTE for MacOS**: this method is implemented using JavaScript. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - View.scrollTo](https://developer.android.com/reference/android/view/View#scrollTo(int,%20int))) ///- iOS ([Official API - UIScrollView.setContentOffset](https://developer.apple.com/documentation/uikit/uiscrollview/1619400-setcontentoffset)) + ///- MacOS ///- Web ([Official API - Window.scrollTo](https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo)) Future scrollTo( {required int x, required int y, bool animated = false}) async { @@ -2261,9 +2298,12 @@ class InAppWebViewController { /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. /// + ///**NOTE for MacOS**: this method is implemented using JavaScript. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - View.scrollBy](https://developer.android.com/reference/android/view/View#scrollBy(int,%20int))) ///- iOS ([Official API - UIScrollView.setContentOffset](https://developer.apple.com/documentation/uikit/uiscrollview/1619400-setcontentoffset)) + ///- MacOS ///- Web ([Official API - Window.scrollBy](https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollBy)) Future scrollBy( {required int x, required int y, bool animated = false}) async { @@ -2277,11 +2317,14 @@ class InAppWebViewController { ///On Android native WebView, it pauses all layout, parsing, and JavaScript timers for all WebViews. ///This is a global requests, not restricted to just this WebView. This can be useful if the application has been paused. /// - ///On iOS, it is implemented using JavaScript and it is restricted to just this WebView. + ///**NOTE for iOS**: it is implemented using JavaScript and it is restricted to just this WebView. + /// + ///**NOTE for MacOS**: it is implemented using JavaScript and it is restricted to just this WebView. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.pauseTimers](https://developer.android.com/reference/android/webkit/WebView#pauseTimers())) ///- iOS + ///- MacOS Future pauseTimers() async { Map args = {}; await _channel.invokeMethod('pauseTimers', args); @@ -2289,11 +2332,14 @@ class InAppWebViewController { ///On Android, it resumes all layout, parsing, and JavaScript timers for all WebViews. This will resume dispatching all timers. /// - ///On iOS, it is implemented using JavaScript and it resumes all layout, parsing, and JavaScript timers to just this WebView. + ///**NOTE for iOS**: it is implemented using JavaScript and it is restricted to just this WebView. + /// + ///**NOTE for MacOS**: it is implemented using JavaScript and it is restricted to just this WebView. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.resumeTimers](https://developer.android.com/reference/android/webkit/WebView#resumeTimers())) ///- iOS + ///- MacOS Future resumeTimers() async { Map args = {}; await _channel.invokeMethod('resumeTimers', args); @@ -2304,13 +2350,16 @@ class InAppWebViewController { ///To obtain the [PrintJobController], use [settings] argument with [PrintJobSettings.handledByClient] to `true`. ///Otherwise this method will return `null` and the [PrintJobController] will be handled and disposed automatically by the system. /// - ///**NOTE**: available on Android 19+. + ///**NOTE for Android**: available on Android 19+. + /// + ///**NOTE for MacOS**: [PrintJobController] is available on MacOS 11.0+. /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. Also, [PrintJobController] is always `null`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - PrintManager.print](https://developer.android.com/reference/android/print/PrintManager#print(java.lang.String,%20android.print.PrintDocumentAdapter,%20android.print.PrintAttributes))) ///- iOS ([Official API - UIPrintInteractionController.present](https://developer.apple.com/documentation/uikit/uiprintinteractioncontroller/1618149-present)) + ///- MacOS (if 11.0+, [Official API - WKWebView.printOperation](https://developer.apple.com/documentation/webkit/wkwebview/3516861-printoperation), else [Official API - NSView.printView](https://developer.apple.com/documentation/appkit/nsview/1483705-printview)) ///- Web ([Official API - Window.print](https://developer.mozilla.org/en-US/docs/Web/API/Window/print)) Future printCurrentPage( {PrintJobSettings? settings}) async { @@ -2327,9 +2376,12 @@ class InAppWebViewController { /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. /// + ///**NOTE for MacOS**: it is implemented using JavaScript. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.getContentHeight](https://developer.android.com/reference/android/webkit/WebView#getContentHeight())) ///- iOS ([Official API - UIScrollView.contentSize](https://developer.apple.com/documentation/uikit/uiscrollview/1619399-contentsize)) + ///- MacOS ///- Web ([Official API - Document.documentElement.scrollHeight](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight)) Future getContentHeight() async { Map args = {}; @@ -2345,6 +2397,33 @@ class InAppWebViewController { return height; } + ///Gets the width of the HTML content. + /// + ///**NOTE for Android**: it is implemented using JavaScript. + /// + ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. + /// + ///**NOTE for MacOS**: it is implemented using JavaScript. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + ///- iOS ([Official API - UIScrollView.contentSize](https://developer.apple.com/documentation/uikit/uiscrollview/1619399-contentsize)) + ///- MacOS + ///- Web ([Official API - Document.documentElement.scrollWidth](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth)) + Future getContentWidth() async { + Map args = {}; + var height = await _channel.invokeMethod('getContentWidth', args); + if (height == null || height == 0) { + // try to use javascript + var scrollHeight = await evaluateJavascript( + source: "document.documentElement.scrollWidth;"); + if (scrollHeight != null && scrollHeight is num) { + height = scrollHeight.toInt(); + } + } + return height; + } + ///Performs a zoom operation in this WebView. /// ///[zoomFactor] represents the zoom factor to apply. On Android, the zoom factor will be clamped to the Webview's zoom limits and, also, this value must be in the range 0.01 (excluded) to 100.0 (included). @@ -2381,6 +2460,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.getOriginalUrl](https://developer.android.com/reference/android/webkit/WebView#getOriginalUrl())) ///- iOS + ///- MacOS ///- Web Future getOriginalUrl() async { Map args = {}; @@ -2415,6 +2495,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future getSelectedText() async { Map args = {}; @@ -2515,6 +2596,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future> getMetaTags() async { List metaTags = []; @@ -2578,13 +2660,14 @@ class InAppWebViewController { ///Returns an instance of [Color] representing the `content` value of the ///`` tag of the current WebView, if available, otherwise `null`. /// - ///**NOTE**: on Android, Web and iOS < 15.0, it is implemented using JavaScript. + ///**NOTE**: on Android, Web, iOS < 15.0 and MacOS < 12.0, it is implemented using JavaScript. /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - WKWebView.themeColor](https://developer.apple.com/documentation/webkit/wkwebview/3794258-themecolor)) + ///- MacOS ([Official API - WKWebView.themeColor](https://developer.apple.com/documentation/webkit/wkwebview/3794258-themecolor)) ///- Web Future getMetaThemeColor() async { Color? themeColor; @@ -2626,9 +2709,12 @@ class InAppWebViewController { /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. /// + ///**NOTE for MacOS**: it is implemented using JavaScript. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - View.getScrollX](https://developer.android.com/reference/android/view/View#getScrollX())) ///- iOS ([Official API - UIScrollView.contentOffset](https://developer.apple.com/documentation/uikit/uiscrollview/1619404-contentoffset)) + ///- MacOS ///- Web ([Official API - Window.scrollX](https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX)) Future getScrollX() async { Map args = {}; @@ -2639,9 +2725,12 @@ class InAppWebViewController { /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. /// + ///**NOTE for MacOS**: it is implemented using JavaScript. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - View.getScrollY](https://developer.android.com/reference/android/view/View#getScrollY())) ///- iOS ([Official API - UIScrollView.contentOffset](https://developer.apple.com/documentation/uikit/uiscrollview/1619404-contentoffset)) + ///- MacOS ///- Web ([Official API - Window.scrollY](https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY)) Future getScrollY() async { Map args = {}; @@ -2653,6 +2742,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.getCertificate](https://developer.android.com/reference/android/webkit/WebView#getCertificate())) ///- iOS + ///- MacOS Future getCertificate() async { Map args = {}; Map? sslCertificateMap = @@ -2663,13 +2753,14 @@ class InAppWebViewController { ///Injects the specified [userScript] into the webpage’s content. /// - ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. - ///There isn't any way to add/remove user scripts specific to iOS window WebViews. - ///This is a limitation of the native iOS WebKit APIs. + ///**NOTE for iOS and MacOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to window WebViews. + ///This is a limitation of the native WebKit APIs. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - WKUserContentController.addUserScript](https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1537448-adduserscript)) + ///- MacOS ([Official API - WKUserContentController.addUserScript](https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1537448-adduserscript)) Future addUserScript({required UserScript userScript}) async { assert(_webview?.windowId == null || defaultTargetPlatform != TargetPlatform.iOS); @@ -2684,13 +2775,14 @@ class InAppWebViewController { ///Injects the [userScripts] into the webpage’s content. /// - ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. - ///There isn't any way to add/remove user scripts specific to iOS window WebViews. - ///This is a limitation of the native iOS WebKit APIs. + ///**NOTE for iOS and MacOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to window WebViews. + ///This is a limitation of the native WebKit APIs. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future addUserScripts({required List userScripts}) async { assert(_webview?.windowId == null || defaultTargetPlatform != TargetPlatform.iOS); @@ -2704,13 +2796,14 @@ class InAppWebViewController { ///User scripts already loaded into the webpage's content cannot be removed. This will have effect only on the next page load. ///Returns `true` if [userScript] was in the list, `false` otherwise. /// - ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. - ///There isn't any way to add/remove user scripts specific to iOS window WebViews. - ///This is a limitation of the native iOS WebKit APIs. + ///**NOTE for iOS and MacOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to window WebViews. + ///This is a limitation of the native WebKit APIs. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future removeUserScript({required UserScript userScript}) async { assert(_webview?.windowId == null || defaultTargetPlatform != TargetPlatform.iOS); @@ -2732,13 +2825,14 @@ class InAppWebViewController { ///Removes all the [UserScript]s with [groupName] as group name from the webpage’s content. ///User scripts already loaded into the webpage's content cannot be removed. This will have effect only on the next page load. /// - ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. - ///There isn't any way to add/remove user scripts specific to iOS window WebViews. - ///This is a limitation of the native iOS WebKit APIs. + ///**NOTE for iOS and MacOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to window WebViews. + ///This is a limitation of the native WebKit APIs. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future removeUserScriptsByGroupName({required String groupName}) async { assert(_webview?.windowId == null || defaultTargetPlatform != TargetPlatform.iOS); @@ -2751,13 +2845,14 @@ class InAppWebViewController { ///Removes the [userScripts] from the webpage’s content. ///User scripts already loaded into the webpage's content cannot be removed. This will have effect only on the next page load. /// - ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. - ///There isn't any way to add/remove user scripts specific to iOS window WebViews. - ///This is a limitation of the native iOS WebKit APIs. + ///**NOTE for iOS and MacOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to window WebViews. + ///This is a limitation of the native WebKit APIs. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future removeUserScripts( {required List userScripts}) async { assert(_webview?.windowId == null || @@ -2770,13 +2865,14 @@ class InAppWebViewController { ///Removes all the user scripts from the webpage’s content. /// - ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. - ///There isn't any way to add/remove user scripts specific to iOS window WebViews. - ///This is a limitation of the native iOS WebKit APIs. + ///**NOTE for iOS and MacOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to window WebViews. + ///This is a limitation of the native WebKit APIs. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - WKUserContentController.removeAllUserScripts](https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1536540-removealluserscripts)) + ///- MacOS ([Official API - WKUserContentController.removeAllUserScripts](https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1536540-removealluserscripts)) Future removeAllUserScripts() async { assert(_webview?.windowId == null || defaultTargetPlatform != TargetPlatform.iOS); @@ -2818,6 +2914,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS ([Official API - WKWebView.callAsyncJavaScript](https://developer.apple.com/documentation/webkit/wkwebview/3656441-callasyncjavascript)) + ///- MacOS ([Official API - WKWebView.callAsyncJavaScript](https://developer.apple.com/documentation/webkit/wkwebview/3656441-callasyncjavascript)) Future callAsyncJavaScript( {required String functionBody, Map arguments = const {}, @@ -2847,11 +2944,14 @@ class InAppWebViewController { /// ///**NOTE for iOS**: Available on iOS 14.0+. If [autoname] is `false`, the [filePath] must ends with the [WebArchiveFormat.WEBARCHIVE] file extension. /// + ///**NOTE for MacOS**: Available on MacOS 11.0+. If [autoname] is `false`, the [filePath] must ends with the [WebArchiveFormat.WEBARCHIVE] file extension. + /// ///**NOTE for Android**: if [autoname] is `false`, the [filePath] must ends with the [WebArchiveFormat.MHT] file extension. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebView.saveWebArchive](https://developer.android.com/reference/android/webkit/WebView#saveWebArchive(java.lang.String,%20boolean,%20android.webkit.ValueCallback%3Cjava.lang.String%3E))) ///- iOS + ///- MacOS Future saveWebArchive( {required String filePath, bool autoname = false}) async { if (!autoname) { @@ -2879,6 +2979,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web ([Official API - Window.isSecureContext](https://developer.mozilla.org/en-US/docs/Web/API/Window/isSecureContext)) Future isSecureContext() async { Map args = {}; @@ -2894,11 +2995,14 @@ class InAppWebViewController { /// ///**NOTE for Android native WebView**: This method should only be called if [WebViewFeature.isFeatureSupported] returns `true` for [WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL]. /// - ///**NOTE**: On iOS, it is implemented using JavaScript. + ///**NOTE for iOS**: it is implemented using JavaScript. + /// + ///**NOTE for MacOS**: it is implemented using JavaScript. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewCompat.createWebMessageChannel](https://developer.android.com/reference/androidx/webkit/WebViewCompat#createWebMessageChannel(android.webkit.WebView))) ///- iOS + ///- MacOS Future createWebMessageChannel() async { Map args = {}; Map? result = @@ -2914,11 +3018,14 @@ class InAppWebViewController { /// ///**NOTE for Android native WebView**: This method should only be called if [WebViewFeature.isFeatureSupported] returns `true` for [WebViewFeature.POST_WEB_MESSAGE]. /// - ///**NOTE**: On iOS, it is implemented using JavaScript. + ///**NOTE for iOS**: it is implemented using JavaScript. + /// + ///**NOTE for MacOS**: it is implemented using JavaScript. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewCompat.postWebMessage](https://developer.android.com/reference/androidx/webkit/WebViewCompat#postWebMessage(android.webkit.WebView,%20androidx.webkit.WebMessageCompat,%20android.net.Uri))) ///- iOS + ///- MacOS Future postWebMessage( {required WebMessage message, Uri? targetOrigin}) async { if (targetOrigin == null) { @@ -3086,11 +3193,14 @@ class InAppWebViewController { /// ///**NOTE for Android**: This method should only be called if [WebViewFeature.isFeatureSupported] returns `true` for [WebViewFeature.WEB_MESSAGE_LISTENER]. /// - ///**NOTE for iOS**: This is implemented using Javascript. + ///**NOTE for iOS**: it is implemented using JavaScript. + /// + ///**NOTE for MacOS**: it is implemented using JavaScript. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebViewCompat.WebMessageListener](https://developer.android.com/reference/androidx/webkit/WebViewCompat#addWebMessageListener(android.webkit.WebView,%20java.lang.String,%20java.util.Set%3Cjava.lang.String%3E,%20androidx.webkit.WebViewCompat.WebMessageListener))) ///- iOS + ///- MacOS Future addWebMessageListener( WebMessageListener webMessageListener) async { assert( @@ -3107,9 +3217,12 @@ class InAppWebViewController { /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. /// + ///**NOTE for MacOS**: it is implemented using JavaScript. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future canScrollVertically() async { Map args = {}; @@ -3120,9 +3233,12 @@ class InAppWebViewController { /// ///**NOTE for Web**: this method will have effect only if the iframe has the same origin. /// + ///**NOTE for MacOS**: it is implemented using JavaScript. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS ///- Web Future canScrollHorizontally() async { Map args = {}; @@ -3233,6 +3349,7 @@ class InAppWebViewController { /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.reloadFromOrigin](https://developer.apple.com/documentation/webkit/wkwebview/1414956-reloadfromorigin)) + ///- MacOS ([Official API - WKWebView.reloadFromOrigin](https://developer.apple.com/documentation/webkit/wkwebview/1414956-reloadfromorigin)) Future reloadFromOrigin() async { Map args = {}; await _channel.invokeMethod('reloadFromOrigin', args); @@ -3245,8 +3362,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available only on iOS 14.0+. /// + ///**NOTE for MacOS**: available only on MacOS 11.0+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.createPdf](https://developer.apple.com/documentation/webkit/wkwebview/3650490-createpdf)) + ///- MacOS ([Official API - WKWebView.createPdf](https://developer.apple.com/documentation/webkit/wkwebview/3650490-createpdf)) Future createPdf( {@Deprecated("Use pdfConfiguration instead") // ignore: deprecated_member_use_from_same_package @@ -3263,8 +3383,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available only on iOS 14.0+. /// + ///**NOTE for MacOS**: available only on MacOS 11.0+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.createWebArchiveData](https://developer.apple.com/documentation/webkit/wkwebview/3650491-createwebarchivedata)) + ///- MacOS ([Official API - WKWebView.createWebArchiveData](https://developer.apple.com/documentation/webkit/wkwebview/3650491-createwebarchivedata)) Future createWebArchiveData() async { Map args = {}; return await _channel.invokeMethod('createWebArchiveData', args); @@ -3274,6 +3397,7 @@ class InAppWebViewController { /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.hasOnlySecureContent](https://developer.apple.com/documentation/webkit/wkwebview/1415002-hasonlysecurecontent)) + ///- MacOS ([Official API - WKWebView.hasOnlySecureContent](https://developer.apple.com/documentation/webkit/wkwebview/1415002-hasonlysecurecontent)) Future hasOnlySecureContent() async { Map args = {}; return await _channel.invokeMethod('hasOnlySecureContent', args); @@ -3283,8 +3407,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available on iOS 15.0+. /// + ///**NOTE for MacOS**: available on MacOS 12.0+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.pauseAllMediaPlayback](https://developer.apple.com/documentation/webkit/wkwebview/3752240-pauseallmediaplayback)). + ///- MacOS ([Official API - WKWebView.pauseAllMediaPlayback](https://developer.apple.com/documentation/webkit/wkwebview/3752240-pauseallmediaplayback)). Future pauseAllMediaPlayback() async { Map args = {}; return await _channel.invokeMethod('pauseAllMediaPlayback', args); @@ -3297,8 +3424,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available on iOS 15.0+. /// + ///**NOTE for MacOS**: available on MacOS 12.0+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.setAllMediaPlaybackSuspended](https://developer.apple.com/documentation/webkit/wkwebview/3752242-setallmediaplaybacksuspended)). + ///- MacOS ([Official API - WKWebView.setAllMediaPlaybackSuspended](https://developer.apple.com/documentation/webkit/wkwebview/3752242-setallmediaplaybacksuspended)). Future setAllMediaPlaybackSuspended({required bool suspended}) async { Map args = {}; args.putIfAbsent("suspended", () => suspended); @@ -3309,8 +3439,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available on iOS 14.5+. /// + ///**NOTE for MacOS**: available on MacOS 11.3+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.closeAllMediaPresentations](https://developer.apple.com/documentation/webkit/wkwebview/3752235-closeallmediapresentations)). + ///- MacOS ([Official API - WKWebView.closeAllMediaPresentations](https://developer.apple.com/documentation/webkit/wkwebview/3752235-closeallmediapresentations)). Future closeAllMediaPresentations() async { Map args = {}; return await _channel.invokeMethod('closeAllMediaPresentations', args); @@ -3322,8 +3455,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available on iOS 15.0+. /// + ///**NOTE for MacOS**: available on MacOS 12.0+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.requestMediaPlaybackState](https://developer.apple.com/documentation/webkit/wkwebview/3752241-requestmediaplaybackstate)). + ///- MacOS ([Official API - WKWebView.requestMediaPlaybackState](https://developer.apple.com/documentation/webkit/wkwebview/3752241-requestmediaplaybackstate)). Future requestMediaPlaybackState() async { Map args = {}; return MediaPlaybackState.fromNativeValue( @@ -3335,6 +3471,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS Future isInFullscreen() async { Map args = {}; return await _channel.invokeMethod('isInFullscreen', args); @@ -3344,8 +3481,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available on iOS 15.0+. /// + ///**NOTE for MacOS**: available on MacOS 12.0+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.cameraCaptureState](https://developer.apple.com/documentation/webkit/wkwebview/3763093-cameracapturestate)). + ///- MacOS ([Official API - WKWebView.cameraCaptureState](https://developer.apple.com/documentation/webkit/wkwebview/3763093-cameracapturestate)). Future getCameraCaptureState() async { Map args = {}; return MediaCaptureState.fromNativeValue( @@ -3356,8 +3496,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available on iOS 15.0+. /// + ///**NOTE for MacOS**: available on MacOS 12.0+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.setCameraCaptureState](https://developer.apple.com/documentation/webkit/wkwebview/3763097-setcameracapturestate)). + ///- MacOS ([Official API - WKWebView.setCameraCaptureState](https://developer.apple.com/documentation/webkit/wkwebview/3763097-setcameracapturestate)). Future setCameraCaptureState({required MediaCaptureState state}) async { Map args = {}; args.putIfAbsent('state', () => state.toNativeValue()); @@ -3368,8 +3511,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available on iOS 15.0+. /// + ///**NOTE for MacOS**: available on MacOS 12.0+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.microphoneCaptureState](https://developer.apple.com/documentation/webkit/wkwebview/3763096-microphonecapturestate)). + ///- MacOS ([Official API - WKWebView.microphoneCaptureState](https://developer.apple.com/documentation/webkit/wkwebview/3763096-microphonecapturestate)). Future getMicrophoneCaptureState() async { Map args = {}; return MediaCaptureState.fromNativeValue( @@ -3380,8 +3526,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available on iOS 15.0+. /// + ///**NOTE for MacOS**: available on MacOS 12.0+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.setMicrophoneCaptureState](https://developer.apple.com/documentation/webkit/wkwebview/3763098-setmicrophonecapturestate)). + ///- MacOS ([Official API - WKWebView.setMicrophoneCaptureState](https://developer.apple.com/documentation/webkit/wkwebview/3763098-setmicrophonecapturestate)). Future setMicrophoneCaptureState( {required MediaCaptureState state}) async { Map args = {}; @@ -3409,8 +3558,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available on iOS 15.0+. /// + ///**NOTE for MacOS**: available on MacOS 12.0+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.loadSimulatedRequest(_:response:responseData:)](https://developer.apple.com/documentation/webkit/wkwebview/3763094-loadsimulatedrequest) and [Official API - WKWebView.loadSimulatedRequest(_:responseHTML:)](https://developer.apple.com/documentation/webkit/wkwebview/3763095-loadsimulatedrequest)). + ///- MacOS ([Official API - WKWebView.loadSimulatedRequest(_:response:responseData:)](https://developer.apple.com/documentation/webkit/wkwebview/3763094-loadsimulatedrequest) and [Official API - WKWebView.loadSimulatedRequest(_:responseHTML:)](https://developer.apple.com/documentation/webkit/wkwebview/3763095-loadsimulatedrequest)). Future loadSimulatedRequest( {required URLRequest urlRequest, required Uint8List data, @@ -3436,6 +3588,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ([Official API - WebSettings.getDefaultUserAgent](https://developer.android.com/reference/android/webkit/WebSettings#getDefaultUserAgent(android.content.Context))) ///- iOS + ///- MacOS static Future getDefaultUserAgent() async { Map args = {}; return await _staticChannel.invokeMethod('getDefaultUserAgent', args); @@ -3545,8 +3698,11 @@ class InAppWebViewController { /// ///**NOTE for iOS**: available only on iOS 11.0+. /// + ///**NOTE for MacOS**: available only on MacOS 10.13+. + /// ///**Supported Platforms/Implementations**: ///- iOS ([Official API - WKWebView.handlesURLScheme](https://developer.apple.com/documentation/webkit/wkwebview/2875370-handlesurlscheme)) + ///- MacOS ([Official API - WKWebView.handlesURLScheme](https://developer.apple.com/documentation/webkit/wkwebview/2875370-handlesurlscheme)) static Future handlesURLScheme(String urlScheme) async { Map args = {}; args.putIfAbsent('urlScheme', () => urlScheme); @@ -3558,6 +3714,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS static Future get tRexRunnerHtml async => await rootBundle.loadString( 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.html'); @@ -3566,6 +3723,7 @@ class InAppWebViewController { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS static Future get tRexRunnerCss async => await rootBundle.loadString( 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.css'); diff --git a/lib/src/in_app_webview/in_app_webview_settings.dart b/lib/src/in_app_webview/in_app_webview_settings.dart index 58e99250..c398fc6b 100755 --- a/lib/src/in_app_webview/in_app_webview_settings.dart +++ b/lib/src/in_app_webview/in_app_webview_settings.dart @@ -1,6 +1,25 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_inappwebview/src/types/user_preferred_content_mode.dart'; +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; +import '../types/action_mode_menu_item.dart'; +import '../types/cache_mode.dart'; +import '../types/data_detector_types.dart'; +import '../types/force_dark.dart'; +import '../types/force_dark_strategy.dart'; +import '../types/layout_algorithm.dart'; +import '../types/mixed_content_mode.dart'; +import '../types/over_scroll_mode.dart'; +import '../types/referrer_policy.dart'; +import '../types/renderer_priority_policy.dart'; +import '../types/requested_with_header_mode.dart'; +import '../types/sandbox.dart'; +import '../types/scrollbar_style.dart'; +import '../types/scrollview_content_inset_adjustment_behavior.dart'; +import '../types/scrollview_deceleration_rate.dart'; +import '../types/selection_granularity.dart'; +import '../types/vertical_scrollbar_position.dart'; import 'android/in_app_webview_options.dart'; import 'apple/in_app_webview_options.dart'; import '../content_blocker.dart'; @@ -12,35 +31,54 @@ import '../android/webview_feature.dart'; import '../in_app_webview/in_app_webview_controller.dart'; import '../context_menu.dart'; +part 'in_app_webview_settings.g.dart'; + +List _deserializeContentBlockers(List? contentBlockersMapList) { + List contentBlockers = []; + if (contentBlockersMapList != null) { + contentBlockersMapList.forEach((contentBlocker) { + contentBlockers.add(ContentBlocker.fromMap( + Map>.from( + Map.from(contentBlocker)))); + }); + } + return contentBlockers; +} + ///This class represents all the WebView settings available. -class InAppWebViewSettings { +@ExchangeableObject(copyMethod: true) +class InAppWebViewSettings_ { ///Set to `true` to be able to listen at the [WebView.shouldOverrideUrlLoading] event. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool useShouldOverrideUrlLoading; + ///- MacOS + bool? useShouldOverrideUrlLoading; ///Set to `true` to be able to listen at the [WebView.onLoadResource] event. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool useOnLoadResource; + ///- MacOS + bool? useOnLoadResource; ///Set to `true` to be able to listen at the [WebView.onDownloadStartRequest] event. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool useOnDownloadStart; + ///- MacOS + bool? useOnDownloadStart; ///Set to `true` to have all the browser's cache cleared before the new WebView is opened. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool clearCache; + ///- MacOS + bool? clearCache; ///Sets the user-agent for the WebView. /// @@ -49,7 +87,8 @@ class InAppWebViewSettings { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - String userAgent; + ///- MacOS + String? userAgent; ///Append to the existing user-agent. Setting userAgent will override this. /// @@ -58,7 +97,8 @@ class InAppWebViewSettings { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - String applicationNameForUserAgent; + ///- MacOS + String? applicationNameForUserAgent; ///Set to `true` to enable JavaScript. The default value is `true`. /// @@ -66,7 +106,8 @@ class InAppWebViewSettings { ///- Android native WebView ///- iOS ///- Web - bool javaScriptEnabled; + ///- MacOS + bool? javaScriptEnabled; ///Set to `true` to allow JavaScript open windows without user interaction. The default value is `false`. /// @@ -76,22 +117,27 @@ class InAppWebViewSettings { ///- Android native WebView ///- iOS ///- Web - bool javaScriptCanOpenWindowsAutomatically; + ///- MacOS + bool? javaScriptCanOpenWindowsAutomatically; ///Set to `true` to prevent HTML5 audio or video from autoplaying. The default value is `true`. /// - ///**NOTE**: available on iOS 10.0+. + ///**NOTE for iOS**: available on iOS 10.0+. + /// + ///**NOTE for MacOS**: available on MacOS 10.12+. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool mediaPlaybackRequiresUserGesture; + ///- MacOS + bool? mediaPlaybackRequiresUserGesture; ///Sets the minimum font size. The default value is `8` for Android, `0` for iOS. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS + ///- MacOS int? minimumFontSize; ///Define whether the vertical scrollbar should be drawn or not. The default value is `true`. @@ -103,7 +149,7 @@ class InAppWebViewSettings { ///- Android native WebView ///- iOS ///- Web - bool verticalScrollBarEnabled; + bool? verticalScrollBarEnabled; ///Define whether the horizontal scrollbar should be drawn or not. The default value is `true`. /// @@ -114,75 +160,93 @@ class InAppWebViewSettings { ///- Android native WebView ///- iOS ///- Web - bool horizontalScrollBarEnabled; + bool? horizontalScrollBarEnabled; ///List of custom schemes that the WebView must handle. Use the [WebView.onLoadResourceWithCustomScheme] event to intercept resource requests with custom scheme. /// - ///**NOTE**: available on iOS 11.0+. + ///**NOTE for iOS**: available on iOS 11.0+. + /// + ///**NOTE for MacOS**: available on MacOS 10.13+. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - List resourceCustomSchemes; + ///- MacOS + List? resourceCustomSchemes; ///List of [ContentBlocker] that are a set of rules used to block content in the browser window. /// - ///**NOTE**: available on iOS 11.0+. + ///**NOTE for iOS**: available on iOS 11.0+. + /// + ///**NOTE for MacOS**: available on MacOS 10.13+. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - List contentBlockers; + ///- MacOS + @ExchangeableObjectProperty(deserializer: _deserializeContentBlockers) + List? contentBlockers; ///Sets the content mode that the WebView needs to use when loading and rendering a webpage. The default value is [UserPreferredContentMode.RECOMMENDED]. /// - ///**NOTE**: available on iOS 13.0+. + ///**NOTE for iOS**: available on iOS 13.0+. + /// + ///**NOTE for MacOS**: available on MacOS 10.15+. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - UserPreferredContentMode? preferredContentMode; + ///- MacOS + UserPreferredContentMode_? preferredContentMode; ///Set to `true` to be able to listen at the [WebView.shouldInterceptAjaxRequest] event. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool useShouldInterceptAjaxRequest; + ///- MacOS + bool? useShouldInterceptAjaxRequest; ///Set to `true` to be able to listen at the [WebView.shouldInterceptFetchRequest] event. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool useShouldInterceptFetchRequest; + ///- MacOS + bool? useShouldInterceptFetchRequest; ///Set to `true` to open a browser window with incognito mode. The default value is `false`. /// - ///**NOTE**: available on iOS 9.0+. - ///On Android, by setting this option to `true`, it will clear all the cookies of all WebView instances, + ///**NOTE for iOS**: available on iOS 9.0+. + /// + ///**NOTE for Android**: setting this to `true`, it will clear all the cookies of all WebView instances, ///because there isn't any way to make the website data store non-persistent for the specific WebView instance such as on iOS. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool incognito; + ///- MacOS + bool? incognito; ///Sets whether WebView should use browser caching. The default value is `true`. /// - ///**NOTE**: available on iOS 9.0+. + ///**NOTE for iOS**: available on iOS 9.0+. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool cacheEnabled; + ///- MacOS + bool? cacheEnabled; ///Set to `true` to make the background of the WebView transparent. If your app has a dark theme, this can prevent a white flash on initialization. The default value is `false`. /// + ///**NOTE for MacOS**: available on MacOS 12.0+. + /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool transparentBackground; + ///- MacOS + bool? transparentBackground; ///Set to `true` to disable vertical scroll. The default value is `false`. /// @@ -192,7 +256,7 @@ class InAppWebViewSettings { ///- Android native WebView ///- iOS ///- Web - bool disableVerticalScroll; + bool? disableVerticalScroll; ///Set to `true` to disable horizontal scroll. The default value is `false`. /// @@ -202,7 +266,7 @@ class InAppWebViewSettings { ///- Android native WebView ///- iOS ///- Web - bool disableHorizontalScroll; + bool? disableHorizontalScroll; ///Set to `true` to disable context menu. The default value is `false`. /// @@ -212,14 +276,15 @@ class InAppWebViewSettings { ///- Android native WebView ///- iOS ///- Web - bool disableContextMenu; + bool? disableContextMenu; ///Set to `false` if the WebView should not support zooming using its on-screen zoom controls and gestures. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool supportZoom; + ///- MacOS + bool? supportZoom; ///Sets whether cross-origin requests in the context of a file scheme URL should be allowed to access content from other file scheme URLs. ///Note that some accesses such as image HTML elements don't follow same-origin rules and aren't affected by this setting. @@ -234,7 +299,8 @@ class InAppWebViewSettings { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool allowFileAccessFromFileURLs; + ///- MacOS + bool? allowFileAccessFromFileURLs; ///Sets whether cross-origin requests in the context of a file scheme URL should be allowed to access content from any origin. ///This includes access to content from other file scheme URLs or web contexts. @@ -249,43 +315,44 @@ class InAppWebViewSettings { ///**Supported Platforms/Implementations**: ///- Android native WebView ///- iOS - bool allowUniversalAccessFromFileURLs; + ///- MacOS + bool? allowUniversalAccessFromFileURLs; ///Sets the text zoom of the page in percent. The default value is `100`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - int textZoom; + int? textZoom; ///Set to `true` to have the session cookie cache cleared before the new window is opened. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool clearSessionCache; + bool? clearSessionCache; ///Set to `true` if the WebView should use its built-in zoom mechanisms. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool builtInZoomControls; + bool? builtInZoomControls; ///Set to `true` if the WebView should display on-screen zoom controls when using the built-in zoom mechanisms. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool displayZoomControls; + bool? displayZoomControls; ///Set to `true` if you want the database storage API is enabled. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool databaseEnabled; + bool? databaseEnabled; ///Set to `true` if you want the DOM storage API is enabled. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool domStorageEnabled; + bool? domStorageEnabled; ///Set to `true` if the WebView should enable support for the "viewport" HTML meta tag or should use a wide viewport. ///When the value of the setting is false, the layout width is always set to the width of the WebView control in device-independent (CSS) pixels. @@ -294,7 +361,7 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool useWideViewPort; + bool? useWideViewPort; ///Sets whether Safe Browsing is enabled. Safe Browsing allows WebView to protect against malware and phishing attacks by verifying the links. ///Safe Browsing is enabled by default for devices which support it. @@ -303,7 +370,7 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool safeBrowsingEnabled; + bool? safeBrowsingEnabled; ///Configures the WebView's behavior when a secure origin attempts to load a resource from an insecure origin. /// @@ -311,20 +378,20 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - MixedContentMode? mixedContentMode; + MixedContentMode_? mixedContentMode; ///Enables or disables content URL access within WebView. Content URL access allows WebView to load content from a content provider installed in the system. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool allowContentAccess; + bool? allowContentAccess; ///Enables or disables file access within WebView. Note that this enables or disables file system access only. ///Assets and resources are still accessible using `file:///android_asset` and `file:///android_res`. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool allowFileAccess; + bool? allowFileAccess; ///Sets the path to the Application Caches files. In order for the Application Caches API to be enabled, this option must be set a path to which the application can write. ///This option is used one time: repeated calls are ignored. @@ -337,44 +404,44 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool blockNetworkImage; + bool? blockNetworkImage; ///Sets whether the WebView should not load resources from the network. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool blockNetworkLoads; + bool? blockNetworkLoads; ///Overrides the way the cache is used. The way the cache is used is based on the navigation type. For a normal page load, the cache is checked and content is re-validated as needed. ///When navigating back, content is not revalidated, instead the content is just retrieved from the cache. The default value is [CacheMode.LOAD_DEFAULT]. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - CacheMode? cacheMode; + CacheMode_? cacheMode; ///Sets the cursive font family name. The default value is `"cursive"`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - String cursiveFontFamily; + String? cursiveFontFamily; ///Sets the default fixed font size. The default value is `16`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - int defaultFixedFontSize; + int? defaultFixedFontSize; ///Sets the default font size. The default value is `16`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - int defaultFontSize; + int? defaultFontSize; ///Sets the default text encoding name to use when decoding html pages. The default value is `"UTF-8"`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - String defaultTextEncodingName; + String? defaultTextEncodingName; ///Disables the action mode menu items according to menuItems flag. /// @@ -382,19 +449,19 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - ActionModeMenuItem? disabledActionModeMenuItems; + ActionModeMenuItem_? disabledActionModeMenuItems; ///Sets the fantasy font family name. The default value is `"fantasy"`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - String fantasyFontFamily; + String? fantasyFontFamily; ///Sets the fixed font family name. The default value is `"monospace"`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - String fixedFontFamily; + String? fixedFontFamily; ///Set the force dark mode for this WebView. The default value is [ForceDark.OFF]. /// @@ -402,7 +469,7 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - ForceDark? forceDark; + ForceDark_? forceDark; ///Set how WebView content should be darkened. ///The default value is [ForceDarkStrategy.PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING]. @@ -411,19 +478,19 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - ForceDarkStrategy? forceDarkStrategy; + ForceDarkStrategy_? forceDarkStrategy; ///Sets whether Geolocation API is enabled. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool geolocationEnabled; + bool? geolocationEnabled; ///Sets the underlying layout algorithm. This will cause a re-layout of the WebView. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - LayoutAlgorithm? layoutAlgorithm; + LayoutAlgorithm_? layoutAlgorithm; ///Sets whether the WebView loads pages in overview mode, that is, zooms out the content to fit on screen by width. ///This setting is taken into account when the content width is greater than the width of the WebView control, for example, when [useWideViewPort] is enabled. @@ -431,7 +498,7 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool loadWithOverviewMode; + bool? loadWithOverviewMode; ///Sets whether the WebView should load image resources. Note that this method controls loading of all images, including those embedded using the data URI scheme. ///Note that if the value of this setting is changed from false to true, all images resources referenced by content currently displayed by the WebView are loaded automatically. @@ -439,13 +506,13 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool loadsImagesAutomatically; + bool? loadsImagesAutomatically; ///Sets the minimum logical font size. The default is `8`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - int minimumLogicalFontSize; + int? minimumLogicalFontSize; ///Sets the initial scale for this WebView. 0 means default. The behavior for the default scale depends on the state of [useWideViewPort] and [loadWithOverviewMode]. ///If the content fits into the WebView control by width, then the zoom is set to 100%. For wide content, the behavior depends on the state of [loadWithOverviewMode]. @@ -456,13 +523,13 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - int initialScale; + int? initialScale; ///Tells the WebView whether it needs to set a node. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool needInitialFocus; + bool? needInitialFocus; ///Sets whether this WebView should raster tiles when it is offscreen but attached to a window. ///Turning this on can avoid rendering artifacts when animating an offscreen WebView on-screen. @@ -472,25 +539,25 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool offscreenPreRaster; + bool? offscreenPreRaster; ///Sets the sans-serif font family name. The default value is `"sans-serif"`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - String sansSerifFontFamily; + String? sansSerifFontFamily; ///Sets the serif font family name. The default value is `"sans-serif"`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - String serifFontFamily; + String? serifFontFamily; ///Sets the standard font family name. The default value is `"sans-serif"`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - String standardFontFamily; + String? standardFontFamily; ///Sets whether the WebView should save form data. In Android O, the platform has implemented a fully functional Autofill feature to store form data. ///Therefore, the Webview form data save feature is disabled. Note that the feature will continue to be supported on older versions of Android as before. @@ -498,7 +565,7 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool saveFormData; + bool? saveFormData; ///Boolean value to enable third party cookies in the WebView. ///Used on Android Lollipop and above only as third party cookies are enabled by default on Android Kitkat and below and on iOS. @@ -508,21 +575,21 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool thirdPartyCookiesEnabled; + bool? thirdPartyCookiesEnabled; ///Boolean value to enable Hardware Acceleration in the WebView. ///The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool hardwareAcceleration; + bool? hardwareAcceleration; ///Sets whether the WebView supports multiple windows. ///If set to `true`, [WebView.onCreateWindow] event must be implemented by the host application. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool supportMultipleWindows; + bool? supportMultipleWindows; ///Regular expression used by [WebView.shouldOverrideUrlLoading] event to cancel navigation requests for frames that are not the main frame. ///If the url request of a subframe matches the regular expression, then the request of that subframe is canceled. @@ -539,19 +606,19 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool useHybridComposition; + bool? useHybridComposition; ///Set to `true` to be able to listen at the [WebView.shouldInterceptRequest] event. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool useShouldInterceptRequest; + bool? useShouldInterceptRequest; ///Set to `true` to be able to listen at the [WebView.onRenderProcessGone] event. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool useOnRenderProcessGone; + bool? useOnRenderProcessGone; ///Sets the WebView's over-scroll mode. ///Setting the over-scroll mode of a WebView will have an effect only if the WebView is capable of scrolling. @@ -559,7 +626,7 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - OverScrollMode? overScrollMode; + OverScrollMode_? overScrollMode; ///Informs WebView of the network state. ///This is used to set the JavaScript property `window.navigator.isOnline` and generates the online/offline event as specified in HTML5, sec. 5.7.7. @@ -577,14 +644,14 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - ScrollBarStyle? scrollBarStyle; + ScrollBarStyle_? scrollBarStyle; ///Sets the position of the vertical scroll bar. ///The default value is [VerticalScrollbarPosition.SCROLLBAR_POSITION_DEFAULT]. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - VerticalScrollbarPosition? verticalScrollbarPosition; + VerticalScrollbarPosition_? verticalScrollbarPosition; ///Defines the delay in milliseconds that a scrollbar waits before fade out. /// @@ -597,7 +664,7 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool scrollbarFadingEnabled; + bool? scrollbarFadingEnabled; ///Defines the scrollbar fade duration in milliseconds. /// @@ -609,14 +676,14 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - RendererPriorityPolicy? rendererPriorityPolicy; + RendererPriorityPolicy_? rendererPriorityPolicy; ///Sets whether the default Android error page should be disabled. ///The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool disableDefaultErrorPage; + bool? disableDefaultErrorPage; ///Sets the vertical scrollbar thumb color. /// @@ -657,7 +724,7 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool willSuppressErrorPage; + bool? willSuppressErrorPage; ///Control whether algorithmic darkening is allowed. /// @@ -677,7 +744,7 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool algorithmicDarkeningAllowed; + bool? algorithmicDarkeningAllowed; ///Sets how the WebView will set the `X-Requested-With` header on requests. ///If you are calling this method, you may also want to call [ServiceWorkerWebSettingsCompat.setRequestedWithHeaderMode] @@ -688,7 +755,7 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - RequestedWithHeaderMode? requestedWithHeaderMode; + RequestedWithHeaderMode_? requestedWithHeaderMode; ///Sets whether EnterpriseAuthenticationAppLinkPolicy if set by admin is allowed to have any ///effect on WebView. @@ -703,37 +770,41 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- Android native WebView - bool enterpriseAuthenticationAppLinkPolicyEnabled; + bool? enterpriseAuthenticationAppLinkPolicyEnabled; ///Set to `true` to disable the bouncing of the WebView when the scrolling has reached an edge of the content. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- iOS - bool disallowOverScroll; + bool? disallowOverScroll; ///Set to `true` to allow a viewport meta tag to either disable or restrict the range of user scaling. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- iOS - bool enableViewportScale; + ///- MacOS + bool? enableViewportScale; ///Set to `true` if you want the WebView suppresses content rendering until it is fully loaded into memory. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- iOS - bool suppressesIncrementalRendering; + ///- MacOS + bool? suppressesIncrementalRendering; ///Set to `true` to allow AirPlay. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- iOS - bool allowsAirPlayForMediaPlayback; + ///- MacOS + bool? allowsAirPlayForMediaPlayback; ///Set to `true` to allow the horizontal swipe gestures trigger back-forward list navigations. The default value is `true`. /// ///**Supported Platforms/Implementations**: ///- iOS - bool allowsBackForwardNavigationGestures; + ///- MacOS + bool? allowsBackForwardNavigationGestures; ///Set to `true` to allow that pressing on a link displays a preview of the destination for the link. The default value is `true`. /// @@ -741,21 +812,22 @@ class InAppWebViewSettings { /// ///**Supported Platforms/Implementations**: ///- iOS - bool allowsLinkPreview; + ///- MacOS + bool? allowsLinkPreview; ///Set to `true` if you want that the WebView should always allow scaling of the webpage, regardless of the author's intent. ///The ignoresViewportScaleLimits property overrides the `user-scalable` HTML property in a webpage. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- iOS - bool ignoresViewportScaleLimits; + bool? ignoresViewportScaleLimits; ///Set to `true` to allow HTML5 media playback to appear inline within the screen layout, using browser-supplied controls rather than native controls. ///For this to work, add the `webkit-playsinline` attribute to any `