diff --git a/client/android/build.gradle b/client/android/build.gradle index 3cdaac958..ea855b667 100644 --- a/client/android/build.gradle +++ b/client/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock index 5b653814a..26fa685f9 100644 --- a/client/ios/Podfile.lock +++ b/client/ios/Podfile.lock @@ -52,6 +52,8 @@ PODS: - SwiftyGif (5.4.3) - url_launcher_ios (0.0.1): - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter DEPENDENCIES: - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) @@ -60,8 +62,9 @@ DEPENDENCIES: - integration_test (from `.symlinks/plugins/integration_test/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - sensors_plus (from `.symlinks/plugins/sensors_plus/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) SPEC REPOS: trunk: @@ -84,9 +87,11 @@ EXTERNAL SOURCES: sensors_plus: :path: ".symlinks/plugins/sensors_plus/ios" shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/ios" + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 @@ -95,12 +100,13 @@ SPEC CHECKSUMS: file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 integration_test: 13825b8a9334a850581300559b8839134b124670 - path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866 sensors_plus: 5717760720f7e6acd96fdbd75b7428f5ad755ec2 - shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 - url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2 + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 diff --git a/client/ios/Runner.xcodeproj/project.pbxproj b/client/ios/Runner.xcodeproj/project.pbxproj index 08c0c1096..ce36cef07 100644 --- a/client/ios/Runner.xcodeproj/project.pbxproj +++ b/client/ios/Runner.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a33..a6b826db2 100644 --- a/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ ( + distinct: true, + converter: (store) => ControlTreeViewModel.fromStore(store, control), + builder: (context, viewModel) { + debugPrint("WebViewControl build: ${control.id}"); + + String url = control.attrString("url", "")!; + if (url == "") { + return const ErrorControl("WebView.url cannot be empty."); + } + + bool javascriptEnabled = + control.attrBool("javascriptEnabled", false)!; + var bgcolor = HexColor.fromString( + Theme.of(context), control.attrString("bgcolor", "")!); + String preventLink = control.attrString("preventLink", "")!; + + if (Platform.isIOS || Platform.isAndroid) { + var controller = WebViewController() + ..setJavaScriptMode(javascriptEnabled + ? JavaScriptMode.unrestricted + : JavaScriptMode.disabled) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) {}, + onPageStarted: (String url) { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: control.id, + eventName: "page_started", + eventData: url); + }, + onPageFinished: (String url) { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: control.id, + eventName: "page_ended", + eventData: url); + }, + onWebResourceError: (WebResourceError error) { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: control.id, + eventName: "web_resource_error", + eventData: error.toString()); + }, + onNavigationRequest: (NavigationRequest request) { + if (preventLink != "" && + request.url.startsWith(preventLink)) { + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ); + if (bgcolor != null) { + controller.setBackgroundColor(bgcolor); + } + controller.loadRequest(Uri.parse(url)); + return WebViewWidget(controller: controller); + } else { + return const ErrorControl( + "WebView control is not supported on this platform yet."); + } + }); + + return constrainedControl(context, result, parent, control); + } +} diff --git a/package/pubspec.yaml b/package/pubspec.yaml index b904eecc6..72045eca3 100644 --- a/package/pubspec.yaml +++ b/package/pubspec.yaml @@ -42,6 +42,8 @@ dependencies: js: ^0.6.5 fl_chart: ^0.64.0 + webview_flutter: ^4.2.4 + dev_dependencies: flutter_test: sdk: flutter diff --git a/sdk/python/packages/flet-core/src/flet_core/__init__.py b/sdk/python/packages/flet-core/src/flet_core/__init__.py index 1fd981e8d..288871bd9 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -204,5 +204,6 @@ from flet_core.vertical_divider import VerticalDivider from flet_core.view import View from flet_core.window_drag_area import WindowDragArea +from flet_core.webview import WebView from flet_core.range_slider import RangeSlider from flet_core.badge import Badge diff --git a/sdk/python/packages/flet-core/src/flet_core/webview.py b/sdk/python/packages/flet-core/src/flet_core/webview.py new file mode 100644 index 000000000..ab58222d1 --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/webview.py @@ -0,0 +1,231 @@ +from enum import Enum +from typing import Any, List, Optional, Union + +from flet_core.constrained_control import ConstrainedControl +from flet_core.control import OptionalNumber +from flet_core.ref import Ref +from flet_core.text_span import TextSpan +from flet_core.types import ( + AnimationValue, + FontWeight, + FontWeightString, + OffsetValue, + ResponsiveNumber, + RotateValue, + ScaleValue, + TextAlign, + TextAlignString, +) + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + + +class WebView(ConstrainedControl): + """ + Easily load webpages while allowing user interaction. + + The `WebView` control is designed exclusively for iOS and Android platforms. + + ## Examples + A simple webview implementation using this class could be like: + + ```python + import flet + + def main(page: flet.Page): + wv = flet.WebView( + "https://flet.dev", + expand=True, + on_page_started=lambda _: print("Page started"), + on_page_ended=lambda _: print("Page ended"), + on_web_resource_error=lambda e: print("Page error:", e.data), + ) + page.add(wv) + + flet.app(main) + ``` + + ## Properties + + ### `url` + + Start the webview by loading the `url` value. + + ### `javascript_enabled` + + Enable or disable the javascript execution of the page. Note that disabling the javascript execution of the page may result unexpected webpage behaviour. + + ### `prevent_link` + + Specify a link to prevent it from downloading. + + ### `bgcolor` + + Set the background color of the webview. + + ## Events + + ### `on_page_started` + + Fires soon as the first loading process of the webpage is started. + + ### `on_page_ended` + + Fires when all the webpage loading processes are ended. + + ### `on_web_resource_error` + + Fires when there is error with loading a webpage resource. + + View docs: [WebView](https://flet.dev/docs/controls/webview) + """ + + def __init__( + self, + url: str, + ref: Optional[Ref] = None, + key: Optional[str] = None, + width: OptionalNumber = None, + height: OptionalNumber = None, + left: OptionalNumber = None, + top: OptionalNumber = None, + right: OptionalNumber = None, + bottom: OptionalNumber = None, + expand: Union[None, bool, int] = None, + col: Optional[ResponsiveNumber] = None, + opacity: OptionalNumber = None, + rotate: RotateValue = None, + scale: ScaleValue = None, + offset: OffsetValue = None, + aspect_ratio: OptionalNumber = None, + animate_opacity: AnimationValue = None, + animate_size: AnimationValue = None, + animate_position: AnimationValue = None, + animate_rotation: AnimationValue = None, + animate_scale: AnimationValue = None, + animate_offset: AnimationValue = None, + on_animation_end=None, + tooltip: Optional[str] = None, + visible: Optional[bool] = None, + disabled: Optional[bool] = None, + data: Any = None, + # + # webview-specific + # + javascript_enabled: bool = True, + prevent_link: str = "none", + bgcolor: Optional[str] = None, + on_page_started=None, + on_page_ended=None, + on_web_resource_error=None, + ): + ConstrainedControl.__init__( + self, + ref=ref, + key=key, + width=width, + height=height, + left=left, + top=top, + right=right, + bottom=bottom, + expand=expand, + col=col, + opacity=opacity, + rotate=rotate, + scale=scale, + offset=offset, + aspect_ratio=aspect_ratio, + animate_opacity=animate_opacity, + animate_size=animate_size, + animate_position=animate_position, + animate_rotation=animate_rotation, + animate_scale=animate_scale, + animate_offset=animate_offset, + on_animation_end=on_animation_end, + tooltip=tooltip, + visible=visible, + disabled=disabled, + data=data, + ) + + self.url = url + self.javascript_enabled = javascript_enabled + self.prevent_link = prevent_link + self.bgcolor = bgcolor + + # events + self.on_page_started = on_page_started + self.on_page_ended = on_page_ended + self.on_web_resource_error = on_web_resource_error + + def _get_control_name(self): + return "webview" + + # bgcolor + @property + def bgcolor(self): + return self._get_attr("bgcolor") + + @bgcolor.setter + def bgcolor(self, value): + self._set_attr("bgcolor", value) + + # url + @property + def url(self): + return self._get_attr("url") + + @url.setter + def url(self, value): + self._set_attr("url", value) + + # javascript_enabled + @property + def javascript_enabled(self): + return self._get_attr("javascriptEnabled") + + @javascript_enabled.setter + def javascript_enabled(self, value): + self._set_attr("javascriptEnabled", value) + + # prevent_link + @property + def prevent_link(self): + return self._get_attr("prevent_link") + + @prevent_link.setter + def prevent_link(self, value): + self._set_attr("prevent_link", value) + + ## EVENTS + + # on_page_started + @property + def on_page_started(self): + return self._get_event_handler("page_started") + + @on_page_started.setter + def on_page_started(self, handler): + self._add_event_handler("page_started", handler) + + # on_page_ended + @property + def on_page_ended(self): + return self._get_event_handler("page_ended") + + @on_page_ended.setter + def on_page_ended(self, handler): + self._add_event_handler("page_ended", handler) + + # on_web_resource_error + @property + def on_web_resource_error(self): + return self._get_event_handler("web_resource_error") + + @on_web_resource_error.setter + def on_web_resource_error(self, handler): + self._add_event_handler("web_resource_error", handler) diff --git a/sdk/python/packages/flet-runtime/src/flet_runtime/app.py b/sdk/python/packages/flet-runtime/src/flet_runtime/app.py index 75fa04825..0f2af50b9 100644 --- a/sdk/python/packages/flet-runtime/src/flet_runtime/app.py +++ b/sdk/python/packages/flet-runtime/src/flet_runtime/app.py @@ -318,6 +318,10 @@ def __connect_internal_sync( if env_port is not None and env_port: port = int(env_port) + env_host = os.getenv("FLET_SERVER_IP") + if env_host is not None and env_host: + host = env_host + uds_path = os.getenv("FLET_SERVER_UDS_PATH") env_assets_dir = os.getenv("FLET_ASSETS_PATH") @@ -406,6 +410,10 @@ async def __connect_internal_async( if env_port is not None and env_port: port = int(env_port) + env_host = os.getenv("FLET_SERVER_IP") + if env_host is not None and env_host: + host = env_host + uds_path = os.getenv("FLET_SERVER_UDS_PATH") env_assets_dir = os.getenv("FLET_ASSETS_PATH") @@ -485,7 +493,7 @@ def __start_flet_server( use_color_emoji, route_url_strategy, ): - server_ip = host if host not in [None, "", "*"] else "127.0.0.1" + server_ip = "127.0.0.1" if host in [None, "", "*"] else host if port == 0: port = get_free_tcp_port() @@ -517,9 +525,9 @@ def __start_flet_server( logger.info(f"Upload path configured: {upload_dir}") fletd_env["FLET_UPLOAD_ROOT_DIR"] = upload_dir - if host not in [None, "", "*"]: + if host not in [None, ""]: logger.info(f"Host binding configured: {host}") - fletd_env["FLET_SERVER_IP"] = host + fletd_env["FLET_SERVER_IP"] = host if host != "*" else "" if host != "127.0.0.1": fletd_env["FLET_ALLOW_REMOTE_HOST_CLIENTS"] = "true" diff --git a/sdk/python/packages/flet/src/flet/cli/commands/run.py b/sdk/python/packages/flet/src/flet/cli/commands/run.py index 11f478e6c..8cf5051ec 100644 --- a/sdk/python/packages/flet/src/flet/cli/commands/run.py +++ b/sdk/python/packages/flet/src/flet/cli/commands/run.py @@ -46,6 +46,13 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: default=None, help="custom TCP port to run Flet app on", ) + parser.add_argument( + "--host", + dest="host", + type=str, + default=None, + help='host to run Flet web app on. Use "*" to listen on all IPs.', + ) parser.add_argument( "--name", dest="app_name", @@ -158,6 +165,7 @@ def handle(self, options: argparse.Namespace) -> None: + [options.script if options.module else script_path], None if options.directory or options.recursive else script_path, port, + options.host, options.app_name, uds_path, options.web, @@ -189,6 +197,7 @@ def __init__( args, script_path, port, + host, page_name, uds_path, web, @@ -201,6 +210,7 @@ def __init__( self.args = args self.script_path = script_path self.port = port + self.host = host self.page_name = page_name self.uds_path = uds_path self.web = web @@ -228,6 +238,8 @@ def start_process(self): p_env["FLET_PAGE_NAME"] = "/".join(Path(self.script_path).parts[-2:]) if self.port is not None: p_env["FLET_SERVER_PORT"] = str(self.port) + if self.host is not None: + p_env["FLET_SERVER_IP"] = str(self.host) if self.page_name: p_env["FLET_PAGE_NAME"] = self.page_name if self.uds_path is not None: