From e0b770d2a827f4e17687e4a63f4b88f67c3e3146 Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Tue, 6 Feb 2024 17:27:34 +0100 Subject: [PATCH] Upgrade --- .vscode/settings.json | 6 +- lib/eval/bridge/http.dart | 2 +- lib/eval/bridge/m_provider.dart | 19 -- lib/eval/model/m_bridge.dart | 78 ----- lib/main.dart | 2 +- .../manga/detail/manga_detail_view.dart | 5 +- lib/modules/manga/home/manga_home_screen.dart | 11 +- lib/modules/manga/reader/reader_view.dart | 5 +- lib/modules/webview/webview.dart | 169 ++++++++--- lib/router/router.dart | 13 +- lib/services/http/cloudflare.dart | 73 ----- lib/services/http/interceptor.dart | 17 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 8 +- packages/desktop_webview_window/CHANGELOG.md | 83 +++++ packages/desktop_webview_window/LICENSE | 201 +++++++++++++ packages/desktop_webview_window/README.md | 77 +++++ .../analysis_options.yaml | 4 + .../desktop_webview_window/example/README.md | 16 + .../example/analysis_options.yaml | 29 ++ .../example/lib/main.dart | 184 ++++++++++++ .../example/linux/CMakeLists.txt | 116 +++++++ .../example/linux/flutter/CMakeLists.txt | 87 ++++++ .../.plugin_symlinks/desktop_webview_window | 1 + .../.plugin_symlinks/path_provider_linux | 1 + .../flutter/generated_plugin_registrant.cc | 15 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 24 ++ .../example/linux/main.cc | 6 + .../example/linux/my_application.cc | 104 +++++++ .../example/linux/my_application.h | 18 ++ .../example/pubspec.lock | 284 ++++++++++++++++++ .../example/pubspec.yaml | 85 ++++++ .../example/test/widget_test.dart | 8 + .../lib/desktop_webview_window.dart | 195 ++++++++++++ .../lib/src/create_configuration.dart | 54 ++++ .../lib/src/message_channel.dart | 17 ++ .../lib/src/title_bar.dart | 255 ++++++++++++++++ .../lib/src/webview.dart | 91 ++++++ .../lib/src/webview_impl.dart | 262 ++++++++++++++++ .../linux/CMakeLists.txt | 33 ++ .../linux/desktop_webview_window_plugin.cc | 246 +++++++++++++++ .../desktop_webview_window_plugin.h | 26 ++ .../linux/message_channel_plugin.cc | 92 ++++++ .../linux/message_channel_plugin.h | 12 + .../linux/webview_window.cc | 242 +++++++++++++++ .../linux/webview_window.h | 63 ++++ packages/desktop_webview_window/pubspec.lock | 181 +++++++++++ packages/desktop_webview_window/pubspec.yaml | 28 ++ .../run_local_test_server.sh | 1 + .../test/desktop_webview_window_test.dart | 1 + pubspec.lock | 140 ++++++++- pubspec.yaml | 10 +- .../flutter/generated_plugin_registrant.cc | 9 +- windows/flutter/generated_plugins.cmake | 3 +- 56 files changed, 3478 insertions(+), 254 deletions(-) delete mode 100644 lib/services/http/cloudflare.dart create mode 100644 packages/desktop_webview_window/CHANGELOG.md create mode 100644 packages/desktop_webview_window/LICENSE create mode 100644 packages/desktop_webview_window/README.md create mode 100644 packages/desktop_webview_window/analysis_options.yaml create mode 100644 packages/desktop_webview_window/example/README.md create mode 100644 packages/desktop_webview_window/example/analysis_options.yaml create mode 100644 packages/desktop_webview_window/example/lib/main.dart create mode 100644 packages/desktop_webview_window/example/linux/CMakeLists.txt create mode 100644 packages/desktop_webview_window/example/linux/flutter/CMakeLists.txt create mode 120000 packages/desktop_webview_window/example/linux/flutter/ephemeral/.plugin_symlinks/desktop_webview_window create mode 120000 packages/desktop_webview_window/example/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux create mode 100644 packages/desktop_webview_window/example/linux/flutter/generated_plugin_registrant.cc create mode 100644 packages/desktop_webview_window/example/linux/flutter/generated_plugin_registrant.h create mode 100644 packages/desktop_webview_window/example/linux/flutter/generated_plugins.cmake create mode 100644 packages/desktop_webview_window/example/linux/main.cc create mode 100644 packages/desktop_webview_window/example/linux/my_application.cc create mode 100644 packages/desktop_webview_window/example/linux/my_application.h create mode 100644 packages/desktop_webview_window/example/pubspec.lock create mode 100644 packages/desktop_webview_window/example/pubspec.yaml create mode 100644 packages/desktop_webview_window/example/test/widget_test.dart create mode 100644 packages/desktop_webview_window/lib/desktop_webview_window.dart create mode 100644 packages/desktop_webview_window/lib/src/create_configuration.dart create mode 100644 packages/desktop_webview_window/lib/src/message_channel.dart create mode 100644 packages/desktop_webview_window/lib/src/title_bar.dart create mode 100644 packages/desktop_webview_window/lib/src/webview.dart create mode 100644 packages/desktop_webview_window/lib/src/webview_impl.dart create mode 100644 packages/desktop_webview_window/linux/CMakeLists.txt create mode 100644 packages/desktop_webview_window/linux/desktop_webview_window_plugin.cc create mode 100644 packages/desktop_webview_window/linux/include/desktop_webview_window/desktop_webview_window_plugin.h create mode 100644 packages/desktop_webview_window/linux/message_channel_plugin.cc create mode 100644 packages/desktop_webview_window/linux/message_channel_plugin.h create mode 100644 packages/desktop_webview_window/linux/webview_window.cc create mode 100644 packages/desktop_webview_window/linux/webview_window.h create mode 100644 packages/desktop_webview_window/pubspec.lock create mode 100644 packages/desktop_webview_window/pubspec.yaml create mode 100644 packages/desktop_webview_window/run_local_test_server.sh create mode 100644 packages/desktop_webview_window/test/desktop_webview_window_test.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index daefef56..9461b093 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,9 @@ ], "rust-analyzer.linkedProjects": [ ".\\native\\hub\\Cargo.toml" - ] + ], + "files.associations": { + "*.html.erb": "erb", + "cstring": "cpp" + } } \ No newline at end of file diff --git a/lib/eval/bridge/http.dart b/lib/eval/bridge/http.dart index cf7e30f9..590fd464 100644 --- a/lib/eval/bridge/http.dart +++ b/lib/eval/bridge/http.dart @@ -27,7 +27,7 @@ class $Client implements $Instance { '': BridgeConstructorDef( BridgeFunctionDef(returns: BridgeTypeAnnotation($type), params: [ BridgeParameter( - 'source', BridgeTypeAnnotation($MSource.$type), false), + 'source', BridgeTypeAnnotation($MSource.$type), true), ], namedParams: [])) }, methods: { diff --git a/lib/eval/bridge/m_provider.dart b/lib/eval/bridge/m_provider.dart index 676f446f..2d4c8423 100644 --- a/lib/eval/bridge/m_provider.dart +++ b/lib/eval/bridge/m_provider.dart @@ -518,21 +518,6 @@ class $MProvider extends MProvider with $Bridge { false), ]), ), - 'http': BridgeMethodDef( - BridgeFunctionDef( - returns: BridgeTypeAnnotation(BridgeTypeRef( - CoreTypes.future, [BridgeTypeRef(CoreTypes.string)])), - params: [ - BridgeParameter( - 'method', - BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)), - false), - BridgeParameter( - 'datas', - BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)), - false), - ]), - ), 'gogoCdnExtractor': BridgeMethodDef( BridgeFunctionDef( returns: BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.future, [ @@ -764,10 +749,6 @@ class $MProvider extends MProvider with $Bridge { } return $String(out); }), - 'http' => $Function((_, __, List<$Value?> args) { - return $Future.wrap(MBridge.http(args[0]!.$reified, args[1]!.$reified) - .then((value) => $String(value))); - }), 'parseHtml' => $Function((_, __, List<$Value?> args) { final res = MBridge.parsHtml(args[0]!.$reified); return $MDocument.wrap(res); diff --git a/lib/eval/model/m_bridge.dart b/lib/eval/model/m_bridge.dart index a6272bd1..8a988e4c 100644 --- a/lib/eval/model/m_bridge.dart +++ b/lib/eval/model/m_bridge.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:developer'; import 'package:bot_toast/bot_toast.dart'; import 'package:dart_eval/dart_eval_bridge.dart'; import 'package:dart_eval/stdlib/core.dart'; @@ -21,20 +20,16 @@ import 'package:mangayomi/services/anime_extractors/sendvid_extractor.dart'; import 'package:mangayomi/services/anime_extractors/sibnet_extractor.dart'; import 'package:mangayomi/services/anime_extractors/streamlare_extractor.dart'; import 'package:mangayomi/services/anime_extractors/streamtape_extractor.dart'; -import 'package:mangayomi/main.dart'; -import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/models/video.dart'; import 'package:mangayomi/services/anime_extractors/streamwish_extractor.dart'; import 'package:mangayomi/services/anime_extractors/vidbom_extractor.dart'; import 'package:mangayomi/services/anime_extractors/voe_extractor.dart'; import 'package:mangayomi/services/anime_extractors/your_upload_extractor.dart'; -import 'package:mangayomi/services/http/cloudflare.dart'; import 'package:mangayomi/utils/cryptoaes/crypto_aes.dart'; import 'package:mangayomi/utils/cryptoaes/deobfuscator.dart'; import 'package:mangayomi/utils/extensions/string_extensions.dart'; import 'package:mangayomi/utils/reg_exp_matcher.dart'; import 'package:xpath_selector_html_parser/xpath_selector_html_parser.dart'; -import 'package:http/http.dart' as hp; import 'package:encrypt/encrypt.dart' as encrypt; class WordSet { @@ -300,79 +295,6 @@ class MBridge { return regCustomMatcher(expression, source, group); } - //http request and also webview - static Future http(String method, String datas) async { - try { - hp.StreamedResponse? res; - - //Get headers - final headersMap = jsonDecode(datas)["headers"] as Map?; - - //Get sourceId - final sourceId = jsonDecode(datas)["sourceId"] as int?; - - //Get body - final bodyMap = jsonDecode(datas)["body"] as Map?; - - final useFormBuilder = - (jsonDecode(datas)["useFormBuilder"] as bool?) ?? false; - - final url = jsonDecode(datas)["url"] as String; - //Convert body Map to Map - Map body = {}; - if (bodyMap != null) { - body = bodyMap.map((key, value) => MapEntry(key.toString(), value)); - } - - //Convert headers Map to Map - Map headers = {}; - if (headersMap != null) { - headers = headersMap - .map((key, value) => MapEntry(key.toString(), value.toString())); - } - - //Get the serie source - final source = sourceId != null ? isar.sources.getSync(sourceId) : null; - - if (useFormBuilder) { - var request = hp.MultipartRequest(method, Uri.parse(url)); - if (bodyMap != null) { - final fields = bodyMap - .map((key, value) => MapEntry(key.toString(), value.toString())); - request.fields.addAll(fields); - } - request.headers.addAll(headers); - - res = await request.send(); - } else { - var request = hp.Request(method, Uri.parse(url)); - - if (bodyMap != null) { - request.body = json.encode(body); - } - - request.headers.addAll(headers); - - res = await request.send(); - } - if (res.statusCode == 403 && (source?.hasCloudflare ?? false)) { - log("Http request: ${res.statusCode}, Cloudflare"); - return await cloudflareBypass( - url: url, sourceId: source!.id.toString()); - } else if (res.statusCode == 200) { - log("Http request: ${res.statusCode}"); - return await res.stream.bytesToString(); - } else { - log("Http request: ${res.statusCode}, reasonPhrase: ${res.reasonPhrase}"); - return "error"; - } - } catch (e) { - log("Http error: $e"); - // botToast(e.toString()); - return "error"; - } - } - static Future> gogoCdnExtractor(String url) async { return await GogoCdnExtractor().videosFromUrl(url); } diff --git a/lib/main.dart b/lib/main.dart index 3c59d114..339749c4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,7 +40,7 @@ void main(List args) async { // Override the default HTTP client. HttpOverrides.global = MyHttpoverrides(); // If running on desktop platforms and web view title bar widget is active, exit. - if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + if (Platform.isLinux) { if (runWebViewTitleBarWidget(args)) { return; } diff --git a/lib/modules/manga/detail/manga_detail_view.dart b/lib/modules/manga/detail/manga_detail_view.dart index 9a37c965..74608516 100644 --- a/lib/modules/manga/detail/manga_detail_view.dart +++ b/lib/modules/manga/detail/manga_detail_view.dart @@ -1444,10 +1444,11 @@ class _MangaDetailViewState extends ConsumerState ? "${source.baseUrl}${widget.manga!.link!}" : widget.manga!.link!; - Map data = { + Map data = { 'url': url, 'sourceId': source.id.toString(), - 'title': manga.name! + 'title': manga.name!, + "hasCloudFlare": source.hasCloudflare ?? false, }; context.push("/mangawebview", extra: data); }, diff --git a/lib/modules/manga/home/manga_home_screen.dart b/lib/modules/manga/home/manga_home_screen.dart index 95fcb0f4..3b1626f9 100644 --- a/lib/modules/manga/home/manga_home_screen.dart +++ b/lib/modules/manga/home/manga_home_screen.dart @@ -181,10 +181,11 @@ class _MangaHomeScreenState extends ConsumerState { Icon(Icons.search, color: Theme.of(context).hintColor)), IconButton( onPressed: () { - Map data = { + Map data = { 'url': widget.source.baseUrl!, 'sourceId': widget.source.id.toString(), - 'title': '' + 'title': '', + "hasCloudFlare": widget.source.hasCloudflare ?? false }; context.push("/mangawebview", extra: data); }, @@ -474,10 +475,12 @@ class _MangaHomeScreenState extends ConsumerState { children: [ IconButton( onPressed: () { - Map data = { + Map data = { 'url': widget.source.baseUrl!, 'sourceId': widget.source.id.toString(), - 'title': '' + 'title': '', + "hasCloudFlare": + widget.source.hasCloudflare ?? false }; context.push("/mangawebview", extra: data); }, diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 3394906a..90541dbf 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -1209,10 +1209,11 @@ class _MangaChapterPageGalleryState String url = chapter.url!.startsWith('/') ? "${source.baseUrl}/${chapter.url!}" : chapter.url!; - Map data = { + Map data = { 'url': url, 'sourceId': source.id.toString(), - 'title': chapter.name! + 'title': chapter.name!, + "hasCloudFlare": source.hasCloudflare ?? false }; context.push("/mangawebview", extra: data); }, diff --git a/lib/modules/webview/webview.dart b/lib/modules/webview/webview.dart index bc1888a1..46628c95 100644 --- a/lib/modules/webview/webview.dart +++ b/lib/modules/webview/webview.dart @@ -1,11 +1,11 @@ // ignore_for_file: depend_on_referenced_packages -import 'dart:convert'; -import 'dart:developer'; import 'dart:io'; import 'package:desktop_webview_window/desktop_webview_window.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_windows_webview/flutter_windows_webview.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/services/http/interceptor.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -17,12 +17,13 @@ class MangaWebView extends ConsumerStatefulWidget { final String url; final String sourceId; final String title; - const MangaWebView({ - super.key, - required this.url, - required this.sourceId, - required this.title, - }); + final bool hasCloudFlare; + const MangaWebView( + {super.key, + required this.url, + required this.sourceId, + required this.title, + required this.hasCloudFlare}); @override ConsumerState createState() => _MangaWebViewState(); @@ -30,6 +31,7 @@ class MangaWebView extends ConsumerStatefulWidget { class _MangaWebViewState extends ConsumerState { final GlobalKey webViewKey = GlobalKey(); + late final MyInAppBrowser _macOSbrowser; double progress = 0; @override @@ -44,25 +46,57 @@ class _MangaWebViewState extends ConsumerState { super.initState(); } - Webview? webview; - _runWebViewDesktop() async { - webview = await WebviewWindow.create( - configuration: CreateConfiguration( - userDataFolderWindows: await getWebViewPath(), - ), - ); - webview! - ..setBrightness(Brightness.dark) - ..launch(widget.url) - ..addOnUrlRequestCallback((url) async { - decodeHtml(webview!, url); - // final newCookie = - // await webview!.evaluateJavaScript("window.document.cookie;"); - // log(newCookie.toString()); - }) - ..onClose.whenComplete(() { - Navigator.pop(context); - }); + final _windowsWebview = FlutterWindowsWebview(); + Webview? _linuxWebview; + void _runWebViewDesktop() async { + if (Platform.isLinux) { + _linuxWebview = await WebviewWindow.create( + configuration: CreateConfiguration( + userDataFolderWindows: await getWebViewPath(), + ), + ); + _linuxWebview! + ..launch(widget.url) + ..onClose.whenComplete(() { + Navigator.pop(context); + }); + } else if (Platform.isWindows && + await FlutterWindowsWebview.isAvailable()) { + _windowsWebview.launchWebview( + widget.url, + WebviewOptions(messageReceiver: (s) { + if (s.substring(0, 2) == "UA") { + MInterceptor.setCookie(_url, s.replaceFirst("UA", "")); + } + }, onNavigation: (url) { + if (Uri.parse(url).host != Uri.parse(widget.url).host) return false; + + _windowsWebview.runScript( + "window.chrome.webview.postMessage(\"UA\" + navigator.userAgent)"); + + _windowsWebview.getCookies(widget.url).then((cookies) { + final cookie = + cookies.entries.map((e) => "${e.key}=${e.value}").join(";"); + MInterceptor.setCookie(_url, "", cookie: cookie); + }); + + return false; + })); + } else if (Platform.isMacOS) { + await _macOSbrowser.openUrlRequest( + urlRequest: URLRequest(url: WebUri(widget.url)), + settings: InAppBrowserClassSettings( + browserSettings: InAppBrowserSettings( + toolbarTopBackgroundColor: Colors.blue, + presentationStyle: ModalPresentationStyle.POPOVER), + webViewSettings: InAppWebViewSettings( + isInspectable: kDebugMode, + useShouldOverrideUrlLoading: true, + useOnLoadResource: true, + ), + ), + ); + } } bool isNotDesktop = false; @@ -85,7 +119,13 @@ class _MangaWebViewState extends ConsumerState { ), leading: IconButton( onPressed: () { - webview!.close(); + if (Platform.isMacOS) { + if (_macOSbrowser.isOpened()) { + _macOSbrowser.close(); + } + } else if (Platform.isLinux) { + _linuxWebview!.close(); + } Navigator.pop(context); }, icon: const Icon(Icons.close)), @@ -161,7 +201,7 @@ class _MangaWebViewState extends ConsumerState { Share.share(_url); } else if (value == 2) { await InAppBrowser.openWithSystemBrowser( - url: Uri.parse(_url)); + url: WebUri(_url)); } else if (value == 3) { CookieManager.instance().deleteAllCookies(); } @@ -234,7 +274,7 @@ class _MangaWebViewState extends ConsumerState { _canGoForward = canGoForward; }); }, - initialUrlRequest: URLRequest(url: Uri.parse(widget.url)), + initialUrlRequest: URLRequest(url: WebUri(widget.url)), ), ), ], @@ -252,25 +292,60 @@ Future getWebViewPath() async { ); } -Future decodeHtml(Webview webview, String url) async { - try { - final html = await webview - .evaluateJavaScript("window.document.documentElement.outerHTML;"); - final ua = await webview.evaluateJavaScript("navigator.userAgent") ?? ""; - final newCookie = - await webview.evaluateJavaScript("window.document.cookie;"); - log(ua); - if (newCookie != null) { - await MInterceptor.setCookie(url, jsonDecode(ua), - cookie: jsonDecode(newCookie)); +class MyInAppBrowser extends InAppBrowser { + MyInAppBrowser({required this.context}); + late BuildContext context; + @override + Future onBrowserCreated() async {} + + @override + void onUpdateVisitedHistory(WebUri? url, bool? isReload) async { + final ua = await webViewController?.evaluateJavascript( + source: "navigator.userAgent") ?? + ""; + await MInterceptor.setCookie(url.toString(), ua); + } + + @override + Future onLoadStart(url) async {} + + @override + Future onLoadStop(url) async {} + + @override + void onProgressChanged(progress) { + if (progress == 100) { + pullToRefreshController?.endRefreshing(); + } + } + + @override + void onExit() { + Navigator.pop(context); + } + + @override + Future shouldOverrideUrlLoading( + navigationAction) async { + var uri = navigationAction.request.url!; + + if (!["http", "https", "file", "chrome", "data", "javascript", "about"] + .contains(uri.scheme)) { + if (await canLaunchUrl(uri)) { + // Launch the App + await launchUrl( + uri, + ); + // and cancel the request + return NavigationActionPolicy.CANCEL; + } } - final res = jsonDecode(html!) as String; + return NavigationActionPolicy.ALLOW; + } - return res == "" || res.isEmpty - ? null - : res; - } catch (_) { - return null; + @override + void onMainWindowWillClose() { + close(); } } diff --git a/lib/router/router.dart b/lib/router/router.dart index fc25d885..d2a827d0 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -360,20 +360,23 @@ class RouterNotifier extends ChangeNotifier { path: "/mangawebview", name: "mangawebview", builder: (context, state) { - final data = state.extra as Map; + final data = state.extra as Map; return MangaWebView( - url: data["url"]!, - sourceId: data["sourceId"]!, - title: data['title']!); + url: data["url"]!, + sourceId: data["sourceId"]!, + title: data['title']!, + hasCloudFlare: data["hasCloudFlare"]!, + ); }, pageBuilder: (context, state) { - final data = state.extra as Map; + final data = state.extra as Map; return transitionPage( key: state.pageKey, child: MangaWebView( url: data["url"]!, sourceId: data["sourceId"]!, title: data['title']!, + hasCloudFlare: data["hasCloudFlare"]!, ), ); }, diff --git a/lib/services/http/cloudflare.dart b/lib/services/http/cloudflare.dart deleted file mode 100644 index da5e5c42..00000000 --- a/lib/services/http/cloudflare.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:io'; -import 'package:desktop_webview_window/desktop_webview_window.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:mangayomi/modules/webview/webview.dart'; -import 'package:mangayomi/services/http/interceptor.dart'; - -Future cloudflareBypass( - {required String url, required String sourceId}) async { - String ua = ""; - bool isOk = false; - String? html; - if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { - final webview = await WebviewWindow.create( - configuration: CreateConfiguration( - windowHeight: 500, - windowWidth: 500, - userDataFolderWindows: await getWebViewPath(), - ), - ); - webview - ..setBrightness(Brightness.dark) - ..launch(url); - - await Future.doWhile(() async { - await Future.delayed(const Duration(seconds: 1)); - html = await decodeHtml(webview, url); - if (html == null || - html!.contains("Just a moment") || - html!.contains("challenges.cloudflare.com")) { - return true; - } - return false; - }); - - isOk = true; - webview.close(); - } else { - HeadlessInAppWebView? headlessWebView; - headlessWebView = HeadlessInAppWebView( - onLoadStop: (controller, u) async { - html = await controller.getHtml(); - await Future.doWhile(() async { - if (html == null || - html!.contains("Just a moment") || - html!.contains("challenges.cloudflare.com")) { - html = await controller.getHtml(); - return true; - } - return false; - }); - html = await controller.getHtml(); - ua = await controller.evaluateJavascript( - source: "navigator.userAgent") ?? - ""; - isOk = true; - headlessWebView!.dispose(); - }, - initialUrlRequest: URLRequest(url: Uri.parse(url)), - ); - - headlessWebView.run(); - await Future.doWhile(() async { - await Future.delayed(const Duration(seconds: 1)); - if (isOk == true) { - return false; - } - return true; - }); - await MInterceptor.setCookie(url, ua); - } - return html!; -} diff --git a/lib/services/http/interceptor.dart b/lib/services/http/interceptor.dart index a21fffeb..6d329d87 100644 --- a/lib/services/http/interceptor.dart +++ b/lib/services/http/interceptor.dart @@ -61,13 +61,15 @@ class MInterceptor { static Future setCookie(String url, String ua, {String? cookie}) async { List cookies = []; - if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { - cookies = cookie! - .split(RegExp('(?<=)(,)(?=[^;]+?=)')) - .where((cookie) => cookie.isNotEmpty) - .toList(); + if (Platform.isWindows || Platform.isLinux) { + cookies = cookie + ?.split(RegExp('(?<=)(,)(?=[^;]+?=)')) + .where((cookie) => cookie.isNotEmpty) + .toList() ?? + []; } else { - cookies = (await _cookieManager.getCookies(url: Uri.parse(url))) + cookies = (await _cookieManager.getCookies( + url: flutter_inappwebview.WebUri(url))) .map((e) => "${e.name}=${e.value}") .toList(); } @@ -80,8 +82,9 @@ class MInterceptor { ); } await setCookiesPref(url); + } + if (ua.isNotEmpty) { final settings = isar.settings.getSync(227); - isar.writeTxnSync(() => isar.settings.putSync(settings!..userAgent = ua)); } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a905fd79..690d5f4b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -34,6 +35,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) webf_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WebfPlugin"); + webf_plugin_register_with_registrar(webf_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index c593ce36..9e8876b8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_video screen_retriever url_launcher_linux + webf window_manager window_to_front ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cbc63ae8..6d18517b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,7 @@ import FlutterMacOS import Foundation -import desktop_webview_window +import flutter_inappwebview_macos import flutter_web_auth_2 import isar_flutter_libs import media_kit_libs_macos_video @@ -15,14 +15,16 @@ import path_provider_foundation import screen_brightness_macos import screen_retriever import share_plus +import shared_preferences_foundation import sqflite import url_launcher_macos import wakelock_plus +import webf import window_manager import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) @@ -32,9 +34,11 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) + WebFPlugin.register(with: registry.registrar(forPlugin: "WebFPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) } diff --git a/packages/desktop_webview_window/CHANGELOG.md b/packages/desktop_webview_window/CHANGELOG.md new file mode 100644 index 00000000..951e1d1b --- /dev/null +++ b/packages/desktop_webview_window/CHANGELOG.md @@ -0,0 +1,83 @@ +## 0.2.3 + +* fix macOS webview window size not working. + +## 0.2.2 + +* fix memory leak on macOS after close webview window. +* Show and Hide Webview window by [@Iri-Hor](https://github.com/Iri-Hor) in [#268](https://github.com/MixinNetwork/flutter-plugins/pull/268) + +## 0.2.1 + +* add Windows attentions to readme. +* fix linux close sub window cause app exited. +* fix linux webview title bar expanded unexpected. +* More control over webview position and size under windows. [#206](https://github.com/MixinNetwork/flutter-plugins/pull/206) by [Lukas Heinze](https://github.com/Iri-Hor) +* fix zone mismatch [#250](https://github.com/MixinNetwork/flutter-plugins/pull/250) by [CD](https://github.com/459217974) +* fix linux webkit2gtk deprecated error [#246](https://github.com/MixinNetwork/flutter-plugins/pull/246) by [Zhiqiang Zhang](https://github.com/zhangzqs) + +## 0.2.0 + +* BREAK CHANGE: bump linux webkit2gtk version to 4.1 + +## 0.1.6 + +* fix WebView render area wrong offset on Windows. + +## 0.1.5 + +* add `close` method for WebView. +* add `onUrlRequest` event for WebView. + +## 0.1.4 + +* support custom userDataFolder on Windows. +* fix open web view failed cause crash on Windows. + +## 0.1.3 + +Remove windows addition import requirements. + +## 0.1.2 + +fix TitleBar reload do not work. + +## 0.1.1 + +fix window title not show on macOS + +## 0.1.0 + +support custom titlebar. + +NOTE: contains break change. more details see readme. + +## 0.0.7 + +1. support `isWebviewAvailable` check. +2. fix `clearAll` crash on Linux if no webview created. + +## 0.0.6 + +fix swift definition conflict on macOS. [flutter-plugins#17](https://github.com/MixinNetwork/flutter-plugins/issues/17) + +## 0.0.5 + +add `setApplicationNameForUserAgent` for append application name to webview user agent. + +## 0.0.4 + +1. implement `addScriptToExecuteOnDocumentCreated` on macOS. +2. add hot key `command + w` to close window. + +## 0.0.3 + +fix linux build + +## 0.0.2 + +* rename project to desktop_webview_window + +## 0.0.1 + +* add Windows, Linux, macOS support. diff --git a/packages/desktop_webview_window/LICENSE b/packages/desktop_webview_window/LICENSE new file mode 100644 index 00000000..28327530 --- /dev/null +++ b/packages/desktop_webview_window/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2021] [Mixin] + + Licensed 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. \ No newline at end of file diff --git a/packages/desktop_webview_window/README.md b/packages/desktop_webview_window/README.md new file mode 100644 index 00000000..9b15ad15 --- /dev/null +++ b/packages/desktop_webview_window/README.md @@ -0,0 +1,77 @@ +# desktop_webview_window + +[![Pub](https://img.shields.io/pub/v/desktop_webview_window.svg)](https://pub.dev/packages/desktop_webview_window) + +Show a webview window on your flutter desktop application. + +| | | | +| -------- | ------- | ---- | +| Windows | ✅ | [Webview2](https://www.nuget.org/packages/Microsoft.Web.WebView2) 1.0.992.28 | +| Linux | ✅ | [WebKitGTK-4.1](https://webkitgtk.org/reference/webkit2gtk/stable/index.html) | +| macOS | ✅ | WKWebview | + +## Getting Started + +1. modify your `main` method. + ```dart + import 'package:desktop_webview_window/desktop_webview_window.dart'; + + void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Add this your main method. + // used to show a webview title bar. + if (runWebViewTitleBarWidget(args)) { + return; + } + + runApp(MyApp()); + } + + ``` + +2. launch WebViewWindow + + ```dart + final webview = await WebviewWindow.create(); + webview.launch("https://example.com"); + ``` + +## linux requirement + +```shell +sudo apt-get install webkit2gtk-4.1 +``` + +## Windows + +### Requirement + +The backend of desktop_webview_window on Windows is WebView2, which requires **WebView2 Runtime** installed. + +[WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2) is ship in box with Windows11, but +it may not installed on Windows10 devices. So you need consider how to distribute the runtime to your users. + +See more: https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution + +For convenience, you can use `WebviewWindow.isWebviewAvailable()` check whether the WebView2 is available. + +### Attention + +The default user data folder of WebView2 is `your_exe_file\WebView2`, which is not a good place to store user data. + +eg. if the application is installed in a read-only directory, the application will crash when WebView2 try to write data. + +you can use `WebviewWindow.create()` to create a webview with a custom user data folder. + +```dart +final webview = await WebviewWindow.create( + confiruation: CreateConfiguration( + userDataFolderWindows: 'your_custom_user_data_folder', + ), +); +``` + +## License + +see [LICENSE](./LICENSE) diff --git a/packages/desktop_webview_window/analysis_options.yaml b/packages/desktop_webview_window/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/desktop_webview_window/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/desktop_webview_window/example/README.md b/packages/desktop_webview_window/example/README.md new file mode 100644 index 00000000..cb7e20cd --- /dev/null +++ b/packages/desktop_webview_window/example/README.md @@ -0,0 +1,16 @@ +# webview_window_example + +Demonstrates how to use the webview_window plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/desktop_webview_window/example/analysis_options.yaml b/packages/desktop_webview_window/example/analysis_options.yaml new file mode 100644 index 00000000..61b6c4de --- /dev/null +++ b/packages/desktop_webview_window/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/desktop_webview_window/example/lib/main.dart b/packages/desktop_webview_window/example/lib/main.dart new file mode 100644 index 00000000..9a411118 --- /dev/null +++ b/packages/desktop_webview_window/example/lib/main.dart @@ -0,0 +1,184 @@ +import 'dart:io'; + +import 'package:desktop_webview_window/desktop_webview_window.dart'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +void main(List args) { + debugPrint('args: $args'); + if (runWebViewTitleBarWidget(args)) { + return; + } + WidgetsFlutterBinding.ensureInitialized(); + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final TextEditingController _controller = TextEditingController( + text: 'https://example.com', + ); + + bool? _webviewAvailable; + + @override + void initState() { + super.initState(); + WebviewWindow.isWebviewAvailable().then((value) { + setState(() { + _webviewAvailable = value; + }); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + actions: [ + IconButton( + onPressed: () async { + final webview = await WebviewWindow.create( + configuration: CreateConfiguration( + windowHeight: 1280, + windowWidth: 720, + title: "ExampleTestWindow", + titleBarTopPadding: Platform.isMacOS ? 20 : 0, + userDataFolderWindows: await _getWebViewPath(), + ), + ); + webview + ..registerJavaScriptMessageHandler("test", (name, body) { + debugPrint('on javaScipt message: $name $body'); + }) + ..setApplicationNameForUserAgent(" WebviewExample/1.0.0") + ..setPromptHandler((prompt, defaultText) { + if (prompt == "test") { + return "Hello World!"; + } else if (prompt == "init") { + return "initial prompt"; + } + return ""; + }) + ..addScriptToExecuteOnDocumentCreated(""" + const mixinContext = { + platform: 'Desktop', + conversation_id: 'conversationId', + immersive: false, + app_version: '1.0.0', + appearance: 'dark', + } + window.MixinContext = { + getContext: function() { + return JSON.stringify(mixinContext) + } + } +""") + ..launch("http://localhost:3000/test.html"); + }, + icon: const Icon(Icons.bug_report), + ) + ], + ), + body: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TextField(controller: _controller), + const SizedBox(height: 16), + TextButton( + onPressed: _webviewAvailable != true ? null : _onTap, + child: const Text('Open'), + ), + const SizedBox(height: 20), + TextButton( + onPressed: () async { + await WebviewWindow.clearAll( + userDataFolderWindows: await _getWebViewPath(), + ); + debugPrint('clear complete'); + }, + child: const Text('Clear all'), + ) + ], + ), + ), + ), + ), + ); + } + + void _onTap() async { + final webview = await WebviewWindow.create( + configuration: CreateConfiguration( + userDataFolderWindows: await _getWebViewPath(), + titleBarTopPadding: Platform.isMacOS ? 20 : 0, + ), + ); + webview + ..setBrightness(Brightness.dark) + ..setApplicationNameForUserAgent(" WebviewExample/1.0.0") + ..launch(_controller.text) + ..addOnUrlRequestCallback((url) { + debugPrint('url: $url'); + final uri = Uri.parse(url); + if (uri.path == '/login_success') { + debugPrint('login success. token: ${uri.queryParameters['token']}'); + webview.close(); + } + }) + ..onClose.whenComplete(() { + debugPrint("on close"); + }); + await Future.delayed(const Duration(seconds: 2)); + for (final javaScript in _javaScriptToEval) { + try { + final ret = await webview.evaluateJavaScript(javaScript); + debugPrint('evaluateJavaScript: $ret'); + } catch (e) { + debugPrint('evaluateJavaScript error: $e \n $javaScript'); + } + } + } +} + +const _javaScriptToEval = [ + """ + function test() { + return; + } + test(); + """, + 'eval({"name": "test", "user_agent": navigator.userAgent})', + '1 + 1', + 'undefined', + '1.0 + 1.0', + '"test"', +]; + +Future _getWebViewPath() async { + final document = await getApplicationDocumentsDirectory(); + return p.join( + document.path, + 'desktop_webview_window', + ); +} diff --git a/packages/desktop_webview_window/example/linux/CMakeLists.txt b/packages/desktop_webview_window/example/linux/CMakeLists.txt new file mode 100644 index 00000000..bdcee330 --- /dev/null +++ b/packages/desktop_webview_window/example/linux/CMakeLists.txt @@ -0,0 +1,116 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "webview_window_example") +set(APPLICATION_ID "com.example.webview_window") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/desktop_webview_window/example/linux/flutter/CMakeLists.txt b/packages/desktop_webview_window/example/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..33fd5801 --- /dev/null +++ b/packages/desktop_webview_window/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/desktop_webview_window/example/linux/flutter/ephemeral/.plugin_symlinks/desktop_webview_window b/packages/desktop_webview_window/example/linux/flutter/ephemeral/.plugin_symlinks/desktop_webview_window new file mode 120000 index 00000000..b1d7eb69 --- /dev/null +++ b/packages/desktop_webview_window/example/linux/flutter/ephemeral/.plugin_symlinks/desktop_webview_window @@ -0,0 +1 @@ +C:/DEV/flutter/mangayomi/packages/desktop_webview_window/ \ No newline at end of file diff --git a/packages/desktop_webview_window/example/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/packages/desktop_webview_window/example/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux new file mode 120000 index 00000000..d595a13f --- /dev/null +++ b/packages/desktop_webview_window/example/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux @@ -0,0 +1 @@ +C:/Users/kodjo/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/packages/desktop_webview_window/example/linux/flutter/generated_plugin_registrant.cc b/packages/desktop_webview_window/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..980f2920 --- /dev/null +++ b/packages/desktop_webview_window/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); +} diff --git a/packages/desktop_webview_window/example/linux/flutter/generated_plugin_registrant.h b/packages/desktop_webview_window/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/packages/desktop_webview_window/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/desktop_webview_window/example/linux/flutter/generated_plugins.cmake b/packages/desktop_webview_window/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..36d78fde --- /dev/null +++ b/packages/desktop_webview_window/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_window +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/desktop_webview_window/example/linux/main.cc b/packages/desktop_webview_window/example/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/packages/desktop_webview_window/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/desktop_webview_window/example/linux/my_application.cc b/packages/desktop_webview_window/example/linux/my_application.cc new file mode 100644 index 00000000..72c1d3a2 --- /dev/null +++ b/packages/desktop_webview_window/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "webview_window_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "webview_window_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/packages/desktop_webview_window/example/linux/my_application.h b/packages/desktop_webview_window/example/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/packages/desktop_webview_window/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/desktop_webview_window/example/pubspec.lock b/packages/desktop_webview_window/example/pubspec.lock new file mode 100644 index 00000000..f328ebdf --- /dev/null +++ b/packages/desktop_webview_window/example/pubspec.lock @@ -0,0 +1,284 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + desktop_webview_window: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.2.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" + source: hosted + version: "1.10.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + url: "https://pub.dev" + source: hosted + version: "5.2.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" +sdks: + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/packages/desktop_webview_window/example/pubspec.yaml b/packages/desktop_webview_window/example/pubspec.yaml new file mode 100644 index 00000000..87026678 --- /dev/null +++ b/packages/desktop_webview_window/example/pubspec.yaml @@ -0,0 +1,85 @@ +name: webview_window_example +description: Demonstrates how to use the webview_window plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.14.0 <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + desktop_webview_window: + # When depending on this package from a real application you should use: + # webview_window: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + path_provider: ^2.0.7 + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/desktop_webview_window/example/test/widget_test.dart b/packages/desktop_webview_window/example/test/widget_test.dart new file mode 100644 index 00000000..570e0e47 --- /dev/null +++ b/packages/desktop_webview_window/example/test/widget_test.dart @@ -0,0 +1,8 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +void main() {} diff --git a/packages/desktop_webview_window/lib/desktop_webview_window.dart b/packages/desktop_webview_window/lib/desktop_webview_window.dart new file mode 100644 index 00000000..cd3cb217 --- /dev/null +++ b/packages/desktop_webview_window/lib/desktop_webview_window.dart @@ -0,0 +1,195 @@ +// You have generated a new plugin project without +// specifying the `--platforms` flag. A plugin project supports no platforms is generated. +// To add platforms, run `flutter create -t plugin --platforms .` under the same +// directory. You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as p; + +import 'src/create_configuration.dart'; +import 'src/message_channel.dart'; +import 'src/webview.dart'; +import 'src/webview_impl.dart'; + +export 'src/create_configuration.dart'; +export 'src/title_bar.dart'; +export 'src/webview.dart'; + +final List _webviews = []; + +class WebviewWindow { + static const MethodChannel _channel = MethodChannel('webview_window'); + + static const _otherIsolateMessageHandler = ClientMessageChannel(); + + static bool _inited = false; + + static void _init() { + if (_inited) { + return; + } + _inited = true; + _channel.setMethodCallHandler((call) async { + try { + return await _handleMethodCall(call); + } catch (e, s) { + debugPrint("method: ${call.method} args: ${call.arguments}"); + debugPrint('handleMethodCall error: $e $s'); + } + }); + _otherIsolateMessageHandler.setMessageHandler((call) async { + try { + return await _handleOtherIsolateMethodCall(call); + } catch (e, s) { + debugPrint('_handleOtherIsolateMethodCall error: $e $s'); + } + }); + } + + /// Check if WebView runtime is available on the current devices. + static Future isWebviewAvailable() async { + return true; + } + + static Future create({ + CreateConfiguration? configuration, + }) async { + configuration ??= CreateConfiguration.platform(); + _init(); + final viewId = await _channel.invokeMethod( + "create", + configuration.toMap(), + ) as int; + final webview = WebviewImpl(viewId, _channel); + _webviews.add(webview); + return webview; + } + + static Future _handleOtherIsolateMethodCall(MethodCall call) async { + final webViewId = call.arguments['webViewId'] as int; + final webView = _webviews + .cast() + .firstWhere((w) => w?.viewId == webViewId, orElse: () => null); + if (webView == null) { + return; + } + switch (call.method) { + case 'onBackPressed': + await webView.back(); + break; + case 'onForwardPressed': + await webView.forward(); + break; + case 'onRefreshPressed': + await webView.reload(); + break; + case 'onStopPressed': + await webView.stop(); + break; + case 'onClosePressed': + webView.close(); + break; + } + } + + static Future _handleMethodCall(MethodCall call) async { + final args = call.arguments as Map; + final viewId = args['id'] as int; + final webview = _webviews + .cast() + .firstWhere((e) => e?.viewId == viewId, orElse: () => null); + assert(webview != null); + if (webview == null) { + return; + } + switch (call.method) { + case "onWindowClose": + _webviews.remove(webview); + webview.onClosed(); + break; + case "onJavaScriptMessage": + webview.onJavaScriptMessage(args['name'], args['body']); + break; + case "runJavaScriptTextInputPanelWithPrompt": + return webview.onRunJavaScriptTextInputPanelWithPrompt( + args['prompt'], + args['defaultText'], + ); + case "onHistoryChanged": + webview.onHistoryChanged(args['canGoBack'], args['canGoForward']); + await _otherIsolateMessageHandler.invokeMethod('onHistoryChanged', { + 'webViewId': viewId, + 'canGoBack': args['canGoBack'] as bool, + 'canGoForward': args['canGoForward'] as bool, + }); + break; + case "onNavigationStarted": + webview.onNavigationStarted(); + await _otherIsolateMessageHandler.invokeMethod('onNavigationStarted', { + 'webViewId': viewId, + }); + break; + case "onUrlRequested": + final url = args['url'] as String; + webview.notifyUrlChanged(url); + await _otherIsolateMessageHandler.invokeMethod('onUrlRequested', { + 'webViewId': viewId, + 'url': url, + }); + break; + case "onWebMessageReceived": + final message = args['message'] as String; + webview.notifyWebMessageReceived(message); + await _otherIsolateMessageHandler.invokeMethod('onWebMessageReceived', { + 'webViewId': viewId, + 'message': message, + }); + break; + case "onNavigationCompleted": + webview.onNavigationCompleted(); + await _otherIsolateMessageHandler + .invokeMethod('onNavigationCompleted', { + 'webViewId': viewId, + }); + break; + default: + return; + } + } + + /// Clear all cookies and storage. + static Future clearAll({ + String userDataFolderWindows = 'webview_window_WebView2', + }) async { + await _channel.invokeMethod('clearAll'); + + // FIXME(boyan01) Move the logic to windows platform if WebView2 provider a way to clean caches. + // https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/user-data-folder#create-user-data-folders + if (Platform.isWindows) { + final Directory webview2Dir; + if (p.isAbsolute(userDataFolderWindows)) { + webview2Dir = Directory(userDataFolderWindows); + } else { + webview2Dir = Directory(p.join( + p.dirname(Platform.resolvedExecutable), userDataFolderWindows)); + } + + if (await (webview2Dir.exists())) { + for (var i = 0; i <= 4; i++) { + try { + await webview2Dir.delete(recursive: true); + break; + } catch (e) { + debugPrint("delete cache failed. retring.... $e"); + } + // wait to ensure all web window has been closed and file handle has been release. + await Future.delayed(const Duration(seconds: 1)); + } + } + } + } +} diff --git a/packages/desktop_webview_window/lib/src/create_configuration.dart b/packages/desktop_webview_window/lib/src/create_configuration.dart new file mode 100644 index 00000000..21cfa743 --- /dev/null +++ b/packages/desktop_webview_window/lib/src/create_configuration.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +class CreateConfiguration { + final int windowWidth; + final int windowHeight; + + /// Position of the top left point of the webview window + final int windowPosX; + final int windowPosY; + + /// the title of window + final String title; + + final int titleBarHeight; + + final int titleBarTopPadding; + + final String userDataFolderWindows; + + final bool useWindowPositionAndSize; + final bool openMaximized; + + const CreateConfiguration({ + this.windowWidth = 1280, + this.windowHeight = 720, + this.windowPosX = 0, + this.windowPosY = 0, + this.title = "", + this.titleBarHeight = 40, + this.titleBarTopPadding = 0, + this.userDataFolderWindows = 'webview_window_WebView2', + this.useWindowPositionAndSize = false, + this.openMaximized = false, + }); + + factory CreateConfiguration.platform() { + return CreateConfiguration( + titleBarTopPadding: Platform.isMacOS ? 24 : 0, + ); + } + + Map toMap() => { + "windowWidth": windowWidth, + "windowHeight": windowHeight, + "windowPosX": windowPosX, + "windowPosY": windowPosY, + "title": title, + "titleBarHeight": titleBarHeight, + "titleBarTopPadding": titleBarTopPadding, + "userDataFolderWindows": userDataFolderWindows, + "useWindowPositionAndSize": useWindowPositionAndSize, + "openMaximized": openMaximized, + }; +} diff --git a/packages/desktop_webview_window/lib/src/message_channel.dart b/packages/desktop_webview_window/lib/src/message_channel.dart new file mode 100644 index 00000000..9e144d86 --- /dev/null +++ b/packages/desktop_webview_window/lib/src/message_channel.dart @@ -0,0 +1,17 @@ +import 'package:flutter/services.dart'; + +typedef MessageHandler = Future Function(MethodCall call); + +class ClientMessageChannel { + const ClientMessageChannel(); + + static const _channel = MethodChannel('webview_message/client_channel'); + + Future invokeMethod(String method, [dynamic arguments]) { + return _channel.invokeMethod(method, arguments); + } + + void setMessageHandler(MessageHandler? handler) { + _channel.setMethodCallHandler(handler); + } +} diff --git a/packages/desktop_webview_window/lib/src/title_bar.dart b/packages/desktop_webview_window/lib/src/title_bar.dart new file mode 100644 index 00000000..3f8d88c9 --- /dev/null +++ b/packages/desktop_webview_window/lib/src/title_bar.dart @@ -0,0 +1,255 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'message_channel.dart'; + +const _channel = ClientMessageChannel(); + +/// runs the title bar +/// title bar is a widget that displays the title of the webview window +/// return true if the args is matchs the title bar +/// +/// [builder] custom TitleBar widget builder. +/// can use [TitleBarWebViewController] to controller the WebView +/// use [TitleBarWebViewState] to triger the title bar status. +/// +bool runWebViewTitleBarWidget( + List args, { + WidgetBuilder? builder, + Color? backgroundColor, + void Function(Object error, StackTrace stack)? onError, +}) { + if (args.isEmpty || args[0] != 'web_view_title_bar') { + return false; + } + final webViewId = int.tryParse(args[1]); + if (webViewId == null) { + return false; + } + final titleBarTopPadding = int.tryParse(args.length > 2 ? args[2] : '0') ?? 0; + runZonedGuarded( + () { + WidgetsFlutterBinding.ensureInitialized(); + runApp(_TitleBarApp( + webViewId: webViewId, + titleBarTopPadding: titleBarTopPadding, + backgroundColor: backgroundColor, + builder: builder ?? _defaultTitleBar, + )); + }, + onError ?? + (e, s) { + debugPrint('WebViewTitleBar: unhandled expections: $e, $s'); + }, + ); + + return true; +} + +mixin TitleBarWebViewController { + static TitleBarWebViewController of(BuildContext context) { + final state = context.findAncestorStateOfType<_TitleBarAppState>(); + assert(state != null, + 'only can find TitleBarWebViewController in widget which run from runWebViewTitleBarWidget'); + return state!; + } + + int get _webViewId; + + /// navigate back + void back() { + _channel.invokeMethod('onBackPressed', { + 'webViewId': _webViewId, + }); + } + + /// navigate forward + void forward() { + _channel.invokeMethod('onForwardPressed', { + 'webViewId': _webViewId, + }); + } + + /// reload the webview + void reload() { + _channel.invokeMethod('onRefreshPressed', { + 'webViewId': _webViewId, + }); + } + + /// stop loading the webview + void stop() { + _channel.invokeMethod('onStopPressed', { + 'webViewId': _webViewId, + }); + } + + /// close the webview + void close() { + _channel.invokeMethod('onClosePressed', { + 'webViewId': _webViewId, + }); + } +} + +class TitleBarWebViewState extends InheritedWidget { + const TitleBarWebViewState({ + Key? key, + required Widget child, + required this.isLoading, + required this.canGoBack, + required this.canGoForward, + required this.url, + }) : super(key: key, child: child); + + final bool isLoading; + final bool canGoBack; + final bool canGoForward; + final String? url; + + static TitleBarWebViewState of(BuildContext context) { + final TitleBarWebViewState? result = + context.dependOnInheritedWidgetOfExactType(); + assert(result != null, 'No WebViewState found in context'); + return result!; + } + + @override + bool updateShouldNotify(TitleBarWebViewState oldWidget) { + return isLoading != oldWidget.isLoading || + canGoBack != oldWidget.canGoBack || + canGoForward != oldWidget.canGoForward; + } +} + +class _TitleBarApp extends StatefulWidget { + const _TitleBarApp({ + Key? key, + required this.webViewId, + required this.titleBarTopPadding, + required this.builder, + this.backgroundColor, + }) : super(key: key); + + final int webViewId; + + final int titleBarTopPadding; + + final WidgetBuilder builder; + + final Color? backgroundColor; + + @override + State<_TitleBarApp> createState() => _TitleBarAppState(); +} + +class _TitleBarAppState extends State<_TitleBarApp> + with TitleBarWebViewController { + bool _canGoBack = false; + bool _canGoForward = false; + + bool _isLoading = false; + + String? _url; + + @override + int get _webViewId => widget.webViewId; + + @override + void initState() { + super.initState(); + _channel.setMessageHandler((call) async { + final args = call.arguments as Map; + final webViewId = args['webViewId'] as int; + if (webViewId != widget.webViewId) { + return; + } + switch (call.method) { + case "onHistoryChanged": + setState(() { + _canGoBack = args['canGoBack'] as bool; + _canGoForward = args['canGoForward'] as bool; + }); + break; + case "onNavigationStarted": + setState(() { + _isLoading = true; + }); + break; + case "onNavigationCompleted": + setState(() { + _isLoading = false; + }); + break; + case "onUrlRequested": + setState(() { + _url = args['url'] as String; + }); + break; + } + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Material( + color: + widget.backgroundColor ?? Theme.of(context).scaffoldBackgroundColor, + child: Padding( + padding: EdgeInsets.only(top: widget.titleBarTopPadding.toDouble()), + child: TitleBarWebViewState( + isLoading: _isLoading, + canGoBack: _canGoBack, + canGoForward: _canGoForward, + url: _url, + child: Builder(builder: widget.builder), + ), + ), + ), + ); + } +} + +Widget _defaultTitleBar(BuildContext context) { + final state = TitleBarWebViewState.of(context); + final controller = TitleBarWebViewController.of(context); + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + padding: EdgeInsets.zero, + splashRadius: 16, + iconSize: 16, + onPressed: !state.canGoBack ? null : controller.back, + icon: const Icon(Icons.arrow_back), + ), + IconButton( + padding: EdgeInsets.zero, + splashRadius: 16, + iconSize: 16, + onPressed: !state.canGoForward ? null : controller.forward, + icon: const Icon(Icons.arrow_forward), + ), + if (state.isLoading) + IconButton( + padding: EdgeInsets.zero, + splashRadius: 16, + iconSize: 16, + onPressed: controller.stop, + icon: const Icon(Icons.close), + ) + else + IconButton( + padding: EdgeInsets.zero, + splashRadius: 16, + iconSize: 16, + onPressed: controller.reload, + icon: const Icon(Icons.refresh), + ), + const Spacer() + ], + ); +} diff --git a/packages/desktop_webview_window/lib/src/webview.dart b/packages/desktop_webview_window/lib/src/webview.dart new file mode 100644 index 00000000..896e287c --- /dev/null +++ b/packages/desktop_webview_window/lib/src/webview.dart @@ -0,0 +1,91 @@ +import 'package:flutter/foundation.dart'; + +/// Handle custom message from JavaScript in your app. +typedef JavaScriptMessageHandler = void Function(String name, dynamic body); + +typedef PromptHandler = String Function(String prompt, String defaultText); + +typedef OnHistoryChangedCallback = void Function( + bool canGoBack, bool canGoForward); + +/// Callback when WebView start to load a URL. +/// [url] is the URL string. +typedef OnUrlRequestCallback = void Function(String url); + +/// Callback when WebView receives a web message +/// [message] constains the webmessage +typedef OnWebMessageReceivedCallback = void Function(String message); + +abstract class Webview { + Future get onClose; + + /// true if the webview is currently loading a page. + ValueListenable get isNavigating; + + /// Install a message handler that you can call from your Javascript code. + /// + /// available: macOS (10.10+) + void registerJavaScriptMessageHandler( + String name, JavaScriptMessageHandler handler); + + /// available: macOS + void unregisterJavaScriptMessageHandler(String name); + + /// available: macOS + void setPromptHandler(PromptHandler? handler); + + /// Navigates to the given URL. + void launch(String url); + + /// change webview theme. + /// + /// available only: macOS (Brightness.dark only 10.14+) + void setBrightness(Brightness? brightness); + + void addScriptToExecuteOnDocumentCreated(String javaScript); + + /// Append a string to the webview's user-agent. + Future setApplicationNameForUserAgent(String applicationName); + + /// Navigate to the previous page in the history. + Future back(); + + /// Navigate to the next page in the history. + Future forward(); + + /// Show or hide webview window + Future setWebviewWindowVisibility(bool visible); + + /// Reload the current page. + Future reload(); + + /// Stop all navigations and pending resource fetches. + Future stop(); + + /// Opens the Browser DevTools in a separate window + Future openDevToolsWindow(); + + /// Register a callback that will be invoked when the webview history changes. + void setOnHistoryChangedCallback(OnHistoryChangedCallback? callback); + + void addOnUrlRequestCallback(OnUrlRequestCallback callback); + + void removeOnUrlRequestCallback(OnUrlRequestCallback callback); + + void addOnWebMessageReceivedCallback(OnWebMessageReceivedCallback callback); + + void removeOnWebMessageReceivedCallback( + OnWebMessageReceivedCallback callback); + + /// Close the web view window. + void close(); + + /// evaluate JavaScript in the web view. + Future evaluateJavaScript(String javaScript); + + /// post a web message as String to the top level document in this WebView + Future postWebMessageAsString(String webMessage); + + /// post a web message as JSON to the top level document in this WebView + Future postWebMessageAsJson(String webMessage); +} diff --git a/packages/desktop_webview_window/lib/src/webview_impl.dart b/packages/desktop_webview_window/lib/src/webview_impl.dart new file mode 100644 index 00000000..a7497259 --- /dev/null +++ b/packages/desktop_webview_window/lib/src/webview_impl.dart @@ -0,0 +1,262 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'webview.dart'; + +class WebviewImpl extends Webview { + final int viewId; + + final MethodChannel channel; + + final Map _javaScriptMessageHandlers = {}; + + bool _closed = false; + + PromptHandler? _promptHandler; + + final _closeCompleter = Completer(); + + OnHistoryChangedCallback? _onHistoryChanged; + + final ValueNotifier _isNaivgating = ValueNotifier(false); + + final Set _onUrlRequestCallbacks = {}; + + final Set _onWebMessageReceivedCallbacks = {}; + + WebviewImpl(this.viewId, this.channel); + + @override + Future get onClose => _closeCompleter.future; + + void onClosed() { + _closed = true; + _closeCompleter.complete(); + } + + void onJavaScriptMessage(String name, dynamic body) { + assert(!_closed); + final handler = _javaScriptMessageHandlers[name]; + assert(handler != null, "handler $name is not registed."); + handler?.call(name, body); + } + + String onRunJavaScriptTextInputPanelWithPrompt( + String prompt, String defaultText) { + assert(!_closed); + return _promptHandler?.call(prompt, defaultText) ?? defaultText; + } + + void onHistoryChanged(bool canGoBack, bool canGoForward) { + assert(!_closed); + _onHistoryChanged?.call(canGoBack, canGoForward); + } + + void onNavigationStarted() { + _isNaivgating.value = true; + } + + void notifyUrlChanged(String url) { + for (final callback in _onUrlRequestCallbacks) { + callback(url); + } + } + + void notifyWebMessageReceived(String message) { + for (final callback in _onWebMessageReceivedCallbacks) { + callback(message); + } + } + + void onNavigationCompleted() { + _isNaivgating.value = false; + } + + @override + ValueListenable get isNavigating => _isNaivgating; + + @override + void registerJavaScriptMessageHandler( + String name, JavaScriptMessageHandler handler) { + if (Platform.isLinux) { + return; + } + assert(!_closed); + if (_closed) { + return; + } + assert(name.isNotEmpty); + assert(!_javaScriptMessageHandlers.containsKey(name)); + _javaScriptMessageHandlers[name] = handler; + channel.invokeMethod("registerJavaScripInterface", { + "viewId": viewId, + "name": name, + }); + } + + @override + void unregisterJavaScriptMessageHandler(String name) { + if (Platform.isLinux) { + return; + } + if (_closed) { + return; + } + channel.invokeMethod("unregisterJavaScripInterface", { + "viewId": viewId, + "name": name, + }); + } + + @override + void setPromptHandler(PromptHandler? handler) { + if (Platform.isLinux) { + return; + } + _promptHandler = handler; + } + + @override + void launch(String url) async { + await channel.invokeMethod("launch", { + "url": url, + "viewId": viewId, + }); + } + + @override + void setBrightness(Brightness? brightness) { + /// -1 : system default + /// 0 : dark + /// 1 : light + if (Platform.isLinux) { + return; + } + channel.invokeMethod("setBrightness", { + "viewId": viewId, + "brightness": brightness?.index ?? -1, + }); + } + + @override + void addScriptToExecuteOnDocumentCreated(String javaScript) { + if (Platform.isLinux) { + return; + } + assert(javaScript.trim().isNotEmpty); + channel.invokeMethod("addScriptToExecuteOnDocumentCreated", { + "viewId": viewId, + "javaScript": javaScript, + }); + } + + @override + Future setApplicationNameForUserAgent(String applicationName) async { + if (Platform.isLinux) { + return; + } + await channel.invokeMethod("setApplicationNameForUserAgent", { + "viewId": viewId, + "applicationName": applicationName, + }); + } + + @override + Future forward() { + return channel.invokeMethod("forward", {"viewId": viewId}); + } + + @override + Future setWebviewWindowVisibility(bool visible) { + return channel.invokeMethod("setWebviewWindowVisibility", { + "viewId": viewId, + "visible": visible, + }); + } + + @override + Future back() { + return channel.invokeMethod("back", {"viewId": viewId}); + } + + @override + Future reload() { + return channel.invokeMethod("reload", {"viewId": viewId}); + } + + @override + Future stop() { + return channel.invokeMethod("stop", {"viewId": viewId}); + } + + @override + Future openDevToolsWindow() { + return channel.invokeMethod('openDevToolsWindow', {"viewId": viewId}); + } + + @override + void setOnHistoryChangedCallback(OnHistoryChangedCallback? callback) { + _onHistoryChanged = callback; + } + + @override + void addOnUrlRequestCallback(OnUrlRequestCallback callback) { + _onUrlRequestCallbacks.add(callback); + } + + @override + void removeOnUrlRequestCallback(OnUrlRequestCallback callback) { + _onUrlRequestCallbacks.remove(callback); + } + + @override + void addOnWebMessageReceivedCallback(OnWebMessageReceivedCallback callback) { + _onWebMessageReceivedCallbacks.add(callback); + } + + @override + void removeOnWebMessageReceivedCallback( + OnWebMessageReceivedCallback callback) { + _onWebMessageReceivedCallbacks.remove(callback); + } + + @override + void close() { + if (_closed) { + return; + } + channel.invokeMethod("close", {"viewId": viewId}); + } + + @override + Future evaluateJavaScript(String javaScript) async { + final dynamic result = await channel.invokeMethod("evaluateJavaScript", { + "viewId": viewId, + "javaScriptString": javaScript, + }); + if (result is String || result == null) { + return result; + } + return json.encode(result); + } + + @override + Future postWebMessageAsString(String webMessage) async { + return channel.invokeMethod("postWebMessageAsString", { + "viewId": viewId, + "webMessage": webMessage, + }); + } + + @override + Future postWebMessageAsJson(String webMessage) async { + return channel.invokeMethod("postWebMessageAsJson", { + "viewId": viewId, + "webMessage": webMessage, + }); + } +} diff --git a/packages/desktop_webview_window/linux/CMakeLists.txt b/packages/desktop_webview_window/linux/CMakeLists.txt new file mode 100644 index 00000000..f5d12ef4 --- /dev/null +++ b/packages/desktop_webview_window/linux/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.10) +set(PROJECT_NAME "desktop_webview_window") +project(${PROJECT_NAME} LANGUAGES CXX) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "desktop_webview_window_plugin") + +find_package(PkgConfig REQUIRED) +pkg_check_modules(WebKit REQUIRED IMPORTED_TARGET webkit2gtk-4.1) + +add_library(${PLUGIN_NAME} SHARED + "desktop_webview_window_plugin.cc" + webview_window.cc + webview_window.h + message_channel_plugin.h + message_channel_plugin.cc + ) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) +target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) +target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::WebKit) + +# List of absolute paths to libraries that should be bundled with the plugin +set(desktop_webview_window_bundled_libraries + "" + PARENT_SCOPE + ) diff --git a/packages/desktop_webview_window/linux/desktop_webview_window_plugin.cc b/packages/desktop_webview_window/linux/desktop_webview_window_plugin.cc new file mode 100644 index 00000000..3c9fcf01 --- /dev/null +++ b/packages/desktop_webview_window/linux/desktop_webview_window_plugin.cc @@ -0,0 +1,246 @@ +#include "include/desktop_webview_window/desktop_webview_window_plugin.h" + +#include +#include + +#include +#include +#include + +#include + +#include "webview_window.h" +#include "message_channel_plugin.h" + +namespace { + +int64_t next_window_id_ = 0; + +} + +#define WEBVIEW_WINDOW_PLUGIN(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), webview_window_plugin_get_type(), \ + WebviewWindowPlugin)) + +struct _WebviewWindowPlugin { + GObject parent_instance; + FlMethodChannel *method_channel; + std::map> *windows; +}; + +G_DEFINE_TYPE(WebviewWindowPlugin, webview_window_plugin, g_object_get_type()) + +// Called when a method call is received from Flutter. +static void webview_window_plugin_handle_method_call( + WebviewWindowPlugin *self, + FlMethodCall *method_call) { + + const gchar *method = fl_method_call_get_name(method_call); + + if (strcmp(method, "create") == 0) { + auto *args = fl_method_call_get_args(method_call); + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + fl_method_call_respond_error(method_call, "0", "create args is not map", nullptr, nullptr); + return; + } + auto width = fl_value_get_int(fl_value_lookup_string(args, "windowWidth")); + auto height = fl_value_get_int(fl_value_lookup_string(args, "windowHeight")); + auto title = fl_value_get_string(fl_value_lookup_string(args, "title")); + auto title_bar_height = fl_value_get_int(fl_value_lookup_string(args, "titleBarHeight")); + + auto window_id = next_window_id_; + g_object_ref(self); + auto webview = std::make_unique( + self->method_channel, window_id, + [self, window_id]() { + self->windows->erase(window_id); + g_object_unref(self); + }, title, width, height, title_bar_height); + self->windows->insert({window_id, std::move(webview)}); + next_window_id_++; + fl_method_call_respond_success(method_call, fl_value_new_int(window_id), nullptr); + } else if (strcmp(method, "launch") == 0) { + auto *args = fl_method_call_get_args(method_call); + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + fl_method_call_respond_error(method_call, "0", "create args is not map", nullptr, nullptr); + return; + } + auto window_id = fl_value_get_int(fl_value_lookup_string(args, "viewId")); + auto url = fl_value_get_string(fl_value_lookup_string(args, "url")); + + if (!self->windows->count(window_id)) { + fl_method_call_respond_error(method_call, "0", "can not found webview for viewId", nullptr, nullptr); + return; + } + + self->windows->at(window_id)->Navigate(url); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, "addScriptToExecuteOnDocumentCreated") == 0) { + auto *args = fl_method_call_get_args(method_call); + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + fl_method_call_respond_error(method_call, "0", "args is not map", nullptr, nullptr); + return; + } + auto window_id = fl_value_get_int(fl_value_lookup_string(args, "viewId")); + auto java_script = fl_value_get_string(fl_value_lookup_string(args, "javaScript")); + + if (!self->windows->count(window_id)) { + fl_method_call_respond_error(method_call, "0", "can not found webview for viewId", nullptr, nullptr); + return; + } + self->windows->at(window_id)->RunJavaScriptWhenContentReady(java_script); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, "clearAll") == 0) { + for (const auto &item: *self->windows) { + item.second->Close(); + } + // If application didn't create a webview, but we called webkit_website_data_manager_clear, there will be a segment fault. + // To avoid crash, we create a fake webview first and then clear all data. + auto *web_view = webkit_web_view_new(); + auto *context = webkit_web_view_get_context(WEBKIT_WEB_VIEW(web_view)); + auto *website_data_manager = webkit_web_context_get_website_data_manager(context); + webkit_website_data_manager_clear(website_data_manager, WEBKIT_WEBSITE_DATA_ALL, 0, + nullptr, nullptr, nullptr); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, "setApplicationNameForUserAgent") == 0) { + auto *args = fl_method_call_get_args(method_call); + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + fl_method_call_respond_error(method_call, + "0", + "setApplicationNameForUserAgent args is not map", + nullptr, + nullptr); + return; + } + auto window_id = fl_value_get_int(fl_value_lookup_string(args, "viewId")); + auto application_name = fl_value_get_string(fl_value_lookup_string(args, "applicationName")); + + if (!self->windows->count(window_id)) { + fl_method_call_respond_error(method_call, "0", "can not found webview for viewId", nullptr, nullptr); + return; + } + self->windows->at(window_id)->SetApplicationNameForUserAgent(application_name); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, "back") == 0) { + auto *args = fl_method_call_get_args(method_call); + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + fl_method_call_respond_error(method_call, "0", "back args is not map", nullptr, nullptr); + return; + } + auto window_id = fl_value_get_int(fl_value_lookup_string(args, "viewId")); + if (!self->windows->count(window_id)) { + fl_method_call_respond_error(method_call, "0", "can not found webview for viewId", nullptr, nullptr); + return; + } + self->windows->at(window_id)->GoBack(); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, "forward") == 0) { + auto *args = fl_method_call_get_args(method_call); + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + fl_method_call_respond_error(method_call, "0", "forward args is not map", nullptr, nullptr); + return; + } + auto window_id = fl_value_get_int(fl_value_lookup_string(args, "viewId")); + if (!self->windows->count(window_id)) { + fl_method_call_respond_error(method_call, "0", "can not found webview for viewId", nullptr, nullptr); + return; + } + self->windows->at(window_id)->GoForward(); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, "reload") == 0) { + auto *args = fl_method_call_get_args(method_call); + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + fl_method_call_respond_error(method_call, "0", "reload args is not map", nullptr, nullptr); + return; + } + auto window_id = fl_value_get_int(fl_value_lookup_string(args, "viewId")); + if (!self->windows->count(window_id)) { + fl_method_call_respond_error(method_call, "0", "can not found webview for viewId", nullptr, nullptr); + return; + } + self->windows->at(window_id)->Reload(); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, "stop") == 0) { + auto *args = fl_method_call_get_args(method_call); + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + fl_method_call_respond_error(method_call, "0", "stop args is not map", nullptr, nullptr); + return; + } + auto window_id = fl_value_get_int(fl_value_lookup_string(args, "viewId")); + if (!self->windows->count(window_id)) { + fl_method_call_respond_error(method_call, "0", "can not found webview for viewId", nullptr, nullptr); + return; + } + self->windows->at(window_id)->StopLoading(); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, "close") == 0) { + auto *args = fl_method_call_get_args(method_call); + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + fl_method_call_respond_error(method_call, "0", "close args is not map", nullptr, nullptr); + return; + } + auto window_id = fl_value_get_int(fl_value_lookup_string(args, "viewId")); + if (!self->windows->count(window_id)) { + fl_method_call_respond_error(method_call, "0", "can not found webview for viewId", nullptr, nullptr); + return; + } + self->windows->at(window_id)->Close(); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, "evaluateJavaScript") == 0) { + auto *args = fl_method_call_get_args(method_call); + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + fl_method_call_respond_error(method_call, "0", "evaluateJavaScript args is not map", nullptr, nullptr); + return; + } + auto window_id = fl_value_get_int(fl_value_lookup_string(args, "viewId")); + if (!self->windows->count(window_id)) { + fl_method_call_respond_error(method_call, "0", "can not found webview for viewId", nullptr, nullptr); + return; + } + auto *js = fl_value_get_string(fl_value_lookup_string(args, "javaScriptString")); + self->windows->at(window_id)->EvaluateJavaScript(js, method_call); + } else { + fl_method_call_respond_not_implemented(method_call, nullptr); + } + +} + +static void webview_window_plugin_dispose(GObject *object) { + delete WEBVIEW_WINDOW_PLUGIN(object)->windows; + g_object_unref(WEBVIEW_WINDOW_PLUGIN(object)->method_channel); + G_OBJECT_CLASS(webview_window_plugin_parent_class)->dispose(object); +} + +static void webview_window_plugin_class_init(WebviewWindowPluginClass *klass) { + G_OBJECT_CLASS(klass)->dispose = webview_window_plugin_dispose; +} + +static void webview_window_plugin_init(WebviewWindowPlugin *self) { + self->windows = new std::map>(); +} + +static void method_call_cb(FlMethodChannel *channel, FlMethodCall *method_call, + gpointer user_data) { + WebviewWindowPlugin *plugin = WEBVIEW_WINDOW_PLUGIN(user_data); + webview_window_plugin_handle_method_call(plugin, method_call); +} + +void desktop_webview_window_plugin_register_with_registrar(FlPluginRegistrar *registrar) { + client_message_channel_plugin_register_with_registrar(registrar); + + WebviewWindowPlugin *plugin = WEBVIEW_WINDOW_PLUGIN( + g_object_new(webview_window_plugin_get_type(), nullptr)); + + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + g_autoptr(FlMethodChannel) channel = + fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), + "webview_window", + FL_METHOD_CODEC(codec)); + g_object_ref(channel); + plugin->method_channel = channel; + fl_method_channel_set_method_call_handler(channel, method_call_cb, + g_object_ref(plugin), + g_object_unref); + + g_object_unref(plugin); +} diff --git a/packages/desktop_webview_window/linux/include/desktop_webview_window/desktop_webview_window_plugin.h b/packages/desktop_webview_window/linux/include/desktop_webview_window/desktop_webview_window_plugin.h new file mode 100644 index 00000000..8e76f6f5 --- /dev/null +++ b/packages/desktop_webview_window/linux/include/desktop_webview_window/desktop_webview_window_plugin.h @@ -0,0 +1,26 @@ +#ifndef FLUTTER_PLUGIN_WEBVIEW_WINDOW_PLUGIN_H_ +#define FLUTTER_PLUGIN_WEBVIEW_WINDOW_PLUGIN_H_ + +#include + +G_BEGIN_DECLS + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +typedef struct _WebviewWindowPlugin WebviewWindowPlugin; +typedef struct { + GObjectClass parent_class; +} WebviewWindowPluginClass; + +FLUTTER_PLUGIN_EXPORT GType webview_window_plugin_get_type(); + +FLUTTER_PLUGIN_EXPORT void desktop_webview_window_plugin_register_with_registrar( + FlPluginRegistrar* registrar); + +G_END_DECLS + +#endif // FLUTTER_PLUGIN_WEBVIEW_WINDOW_PLUGIN_H_ diff --git a/packages/desktop_webview_window/linux/message_channel_plugin.cc b/packages/desktop_webview_window/linux/message_channel_plugin.cc new file mode 100644 index 00000000..dc8415f5 --- /dev/null +++ b/packages/desktop_webview_window/linux/message_channel_plugin.cc @@ -0,0 +1,92 @@ +// +// Created by boyan on 11/23/21. +// + + +#include "message_channel_plugin.h" + +#include + +#include "glib.h" + +namespace { + +class ClientMessageChannelPlugin { + + public: + explicit ClientMessageChannelPlugin(FlMethodChannel *channel); + + void DispatchMethodCall(FlMethodCall *call) { + auto *name = fl_method_call_get_name(call); + auto *args = fl_method_call_get_args(call); + fl_method_channel_invoke_method(channel_, name, args, nullptr, nullptr, nullptr); + } + + ~ClientMessageChannelPlugin(); + + private: + FlMethodChannel *channel_; +}; + +class ServerMessageChannelPlugin { + + public: + + void AddClient(ClientMessageChannelPlugin *client) { + clients_.insert(client); + } + + void RemoveClient(ClientMessageChannelPlugin *client) { + clients_.erase(client); + } + + void DispatchMethodCall(FlMethodCall *call, ClientMessageChannelPlugin *client_from) { + for (auto client: clients_) { + if (client != client_from) { + client->DispatchMethodCall(call); + } + } + } + + private: + std::set clients_; + +}; + +ServerMessageChannelPlugin *g_server_message_channel_plugin = nullptr; + +ClientMessageChannelPlugin::ClientMessageChannelPlugin(FlMethodChannel *channel) : channel_(channel) { + g_object_ref(channel_); + g_server_message_channel_plugin->AddClient(this); +} + +ClientMessageChannelPlugin::~ClientMessageChannelPlugin() { + g_object_unref(channel_); + g_server_message_channel_plugin->RemoveClient(this); +} + +void client_plugin_proxy_dispatch_method_call(FlMethodChannel *channel, FlMethodCall *call, gpointer user_data) { + auto *client = static_cast(user_data); + g_assert(g_server_message_channel_plugin); + g_server_message_channel_plugin->DispatchMethodCall(call, client); + fl_method_call_respond_success(call, nullptr, nullptr); +} + +} + +void client_message_channel_plugin_register_with_registrar(FlPluginRegistrar *registrar) { + if (!g_server_message_channel_plugin) { + g_server_message_channel_plugin = new ServerMessageChannelPlugin(); + } + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + FlMethodChannel *channel = fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), + "webview_message/client_channel", + FL_METHOD_CODEC(codec)); + auto *client_message_channel_plugin = new ClientMessageChannelPlugin(channel); + fl_method_channel_set_method_call_handler(channel, + client_plugin_proxy_dispatch_method_call, + client_message_channel_plugin, + [](gpointer pointer) { + delete static_cast(pointer); + }); +} diff --git a/packages/desktop_webview_window/linux/message_channel_plugin.h b/packages/desktop_webview_window/linux/message_channel_plugin.h new file mode 100644 index 00000000..637a4f5c --- /dev/null +++ b/packages/desktop_webview_window/linux/message_channel_plugin.h @@ -0,0 +1,12 @@ +// +// Created by boyan on 11/23/21. +// + +#ifndef DESKTOP_WEBVIEW_WINDOW_LINUX_MESSAGE_CHANNEL_PLUGIN_H_ +#define DESKTOP_WEBVIEW_WINDOW_LINUX_MESSAGE_CHANNEL_PLUGIN_H_ + +#include + +void client_message_channel_plugin_register_with_registrar(FlPluginRegistrar *registrar); + +#endif //DESKTOP_WEBVIEW_WINDOW_LINUX_MESSAGE_CHANNEL_PLUGIN_H_ diff --git a/packages/desktop_webview_window/linux/webview_window.cc b/packages/desktop_webview_window/linux/webview_window.cc new file mode 100644 index 00000000..fc8c176b --- /dev/null +++ b/packages/desktop_webview_window/linux/webview_window.cc @@ -0,0 +1,242 @@ +// +// Created by boyan on 10/21/21. +// + +#include "webview_window.h" + +#include + +#include "message_channel_plugin.h" + +namespace { + +gboolean on_load_failed_with_tls_errors( + WebKitWebView *web_view, + char *failing_uri, + GTlsCertificate *certificate, + GTlsCertificateFlags errors, + gpointer user_data) { + auto *webview = static_cast(user_data); + g_critical("on_load_failed_with_tls_errors: %s %p error= %d", failing_uri, webview, errors); + // TODO allow certificate for some certificate ? + // maybe we can use the pem from https://source.chromium.org/chromium/chromium/src/+/master:net/data/ssl/ev_roots/ +// webkit_web_context_allow_tls_certificate_for_host(webkit_web_view_get_context(web_view), certificate, uri->host); +// webkit_web_view_load_uri(web_view, failing_uri); + return false; +} + +GtkWidget *on_create(WebKitWebView *web_view, + WebKitNavigationAction *navigation_action, + gpointer user_data) { + return GTK_WIDGET(web_view); +} + +void on_load_changed(WebKitWebView *web_view, + WebKitLoadEvent load_event, + gpointer user_data) { + auto *window = static_cast(user_data); + window->OnLoadChanged(load_event); +} + +gboolean decide_policy_cb(WebKitWebView *web_view, + WebKitPolicyDecision *decision, + WebKitPolicyDecisionType type, + gpointer user_data) { + auto *window = static_cast(user_data); + return window->DecidePolicy(decision, type); +} + +} + +WebviewWindow::WebviewWindow( + FlMethodChannel *method_channel, + int64_t window_id, + std::function on_close_callback, + const std::string &title, + int width, + int height, + int title_bar_height +) : method_channel_(method_channel), + window_id_(window_id), + on_close_callback_(std::move(on_close_callback)), + default_user_agent_() { + g_object_ref(method_channel_); + + window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); + g_signal_connect(G_OBJECT(window_), "destroy", + G_CALLBACK(+[](GtkWidget *, gpointer arg) { + auto *window = static_cast(arg); + if (window->on_close_callback_) { + window->on_close_callback_(); + } + auto *args = fl_value_new_map(); + fl_value_set(args, fl_value_new_string("id"), fl_value_new_int(window->window_id_)); + fl_method_channel_invoke_method( + FL_METHOD_CHANNEL(window->method_channel_), "onWindowClose", args, + nullptr, nullptr, nullptr); + }), this); + gtk_window_set_title(GTK_WINDOW(window_), title.c_str()); + gtk_window_set_default_size(GTK_WINDOW(window_), width, height); + gtk_window_set_position(GTK_WINDOW(window_), GTK_WIN_POS_CENTER); + + box_ = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)); + gtk_container_add(GTK_CONTAINER(window_), GTK_WIDGET(box_)); + + // initial flutter_view + g_autoptr(FlDartProject) project = fl_dart_project_new(); + const char *args[] = {"web_view_title_bar", g_strdup_printf("%ld", window_id), nullptr}; + fl_dart_project_set_dart_entrypoint_arguments(project, const_cast(args)); + auto *title_bar = fl_view_new(project); + + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(FL_PLUGIN_REGISTRY(title_bar), "DesktopWebviewWindowPlugin"); + client_message_channel_plugin_register_with_registrar(desktop_webview_window_registrar); + + gtk_widget_set_size_request(GTK_WIDGET(title_bar), -1, title_bar_height); + gtk_widget_set_vexpand(GTK_WIDGET(title_bar), FALSE); + gtk_box_pack_start(box_, GTK_WIDGET(title_bar), FALSE, FALSE, 0); + + // initial web_view + webview_ = webkit_web_view_new(); + g_signal_connect(G_OBJECT(webview_), "load-failed-with-tls-errors", + G_CALLBACK(on_load_failed_with_tls_errors), this); + g_signal_connect(G_OBJECT(webview_), "create", + G_CALLBACK(on_create), this); + g_signal_connect(G_OBJECT(webview_), "load-changed", + G_CALLBACK(on_load_changed), this); + g_signal_connect(G_OBJECT(webview_), "decide-policy", + G_CALLBACK(decide_policy_cb), this); + + auto settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview_)); + webkit_settings_set_javascript_can_open_windows_automatically(settings, true); + default_user_agent_ = webkit_settings_get_user_agent(settings); + gtk_box_pack_end(box_, webview_, true, true, 0); + + gtk_widget_show_all(GTK_WIDGET(window_)); + gtk_widget_grab_focus(GTK_WIDGET(webview_)); + + // FROM: https://github.com/leanflutter/window_manager/pull/343 + // Disconnect all delete-event handlers first in flutter 3.10.1, which causes delete_event not working. + // Issues from flutter/engine: https://github.com/flutter/engine/pull/40033 + guint handler_id = g_signal_handler_find(window_, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, title_bar); + if (handler_id > 0) { + g_signal_handler_disconnect(window_, handler_id); + } +} + +WebviewWindow::~WebviewWindow() { + g_object_unref(method_channel_); + printf("~WebviewWindow\n"); +} + +void WebviewWindow::Navigate(const char *url) { + webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webview_), url); +} + +void WebviewWindow::RunJavaScriptWhenContentReady(const char *java_script) { + auto *manager = webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(webview_)); + webkit_user_content_manager_add_script( + manager, + webkit_user_script_new(java_script, + WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, + WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, + nullptr, + nullptr)); +} + +void WebviewWindow::SetApplicationNameForUserAgent(const std::string &app_name) { + auto *setting = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview_)); + webkit_settings_set_user_agent(setting, (default_user_agent_ + app_name).c_str()); +} + +void WebviewWindow::Close() { + gtk_window_close(GTK_WINDOW(window_)); +} + +void WebviewWindow::OnLoadChanged(WebKitLoadEvent load_event) { + // notify history changed event. + { + auto can_go_back = webkit_web_view_can_go_back(WEBKIT_WEB_VIEW(webview_)); + auto can_go_forward = webkit_web_view_can_go_forward(WEBKIT_WEB_VIEW(webview_)); + auto *args = fl_value_new_map(); + fl_value_set(args, fl_value_new_string("id"), fl_value_new_int(window_id_)); + fl_value_set(args, fl_value_new_string("canGoBack"), fl_value_new_bool(can_go_back)); + fl_value_set(args, fl_value_new_string("canGoForward"), fl_value_new_bool(can_go_forward)); + fl_method_channel_invoke_method( + FL_METHOD_CHANNEL(method_channel_), "onHistoryChanged", args, + nullptr, nullptr, nullptr); + } + + // notify load start/finished event. + switch (load_event) { + case WEBKIT_LOAD_STARTED: { + auto *args = fl_value_new_map(); + fl_value_set(args, fl_value_new_string("id"), fl_value_new_int(window_id_)); + fl_method_channel_invoke_method( + FL_METHOD_CHANNEL(method_channel_), "onNavigationStarted", args, + nullptr, nullptr, nullptr); + break; + } + case WEBKIT_LOAD_FINISHED: { + auto *args = fl_value_new_map(); + fl_value_set(args, fl_value_new_string("id"), fl_value_new_int(window_id_)); + fl_method_channel_invoke_method( + FL_METHOD_CHANNEL(method_channel_), "onNavigationCompleted", args, + nullptr, nullptr, nullptr); + break; + } + default :break; + } + +} + +void WebviewWindow::GoForward() { + webkit_web_view_go_forward(WEBKIT_WEB_VIEW(webview_)); +} + +void WebviewWindow::GoBack() { + webkit_web_view_go_back(WEBKIT_WEB_VIEW(webview_)); +} + +void WebviewWindow::Reload() { + webkit_web_view_reload(WEBKIT_WEB_VIEW(webview_)); +} + +void WebviewWindow::StopLoading() { + webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(webview_)); +} + +gboolean WebviewWindow::DecidePolicy(WebKitPolicyDecision *decision, WebKitPolicyDecisionType type) { + if (type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { + auto *navigation_decision = WEBKIT_NAVIGATION_POLICY_DECISION (decision); + auto *navigation_action = webkit_navigation_policy_decision_get_navigation_action(navigation_decision); + auto *request = webkit_navigation_action_get_request(navigation_action); + auto *uri = webkit_uri_request_get_uri(request); + auto *args = fl_value_new_map(); + fl_value_set(args, fl_value_new_string("id"), fl_value_new_int(window_id_)); + fl_value_set(args, fl_value_new_string("url"), fl_value_new_string(uri)); + fl_method_channel_invoke_method( + FL_METHOD_CHANNEL(method_channel_), "onUrlRequested", args, + nullptr, nullptr, nullptr); + } + return false; +} + +void WebviewWindow::EvaluateJavaScript(const char *java_script, FlMethodCall *call) { + webkit_web_view_evaluate_javascript( + WEBKIT_WEB_VIEW(webview_), java_script, -1, nullptr, nullptr, nullptr, + [](GObject *object, GAsyncResult *result, gpointer user_data) { + auto *call = static_cast(user_data); + GError *error = nullptr; + auto *js_result = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(object), result, &error); + if (!js_result) { + fl_method_call_respond_error(call, "failed to evaluate javascript.", error->message, nullptr, nullptr); + g_error_free(error); + } else { + auto *js_value = jsc_value_to_json(js_result, 0); + fl_method_call_respond_success(call, js_value ? fl_value_new_string(js_value) : nullptr, nullptr); + } + g_object_unref(call); + }, + g_object_ref(call)); +} \ No newline at end of file diff --git a/packages/desktop_webview_window/linux/webview_window.h b/packages/desktop_webview_window/linux/webview_window.h new file mode 100644 index 00000000..37706c33 --- /dev/null +++ b/packages/desktop_webview_window/linux/webview_window.h @@ -0,0 +1,63 @@ +// +// Created by boyan on 10/21/21. +// + +#ifndef WEBVIEW_WINDOW_LINUX_WEBVIEW_WINDOW_H_ +#define WEBVIEW_WINDOW_LINUX_WEBVIEW_WINDOW_H_ + +#include +#include +#include +#include + +#include + +class WebviewWindow { + public: + WebviewWindow( + FlMethodChannel *method_channel, + int64_t window_id, + std::function on_close_callback, + const std::string &title, int width, int height, + int title_bar_height + ); + + virtual ~WebviewWindow(); + + void Navigate(const char *url); + + void RunJavaScriptWhenContentReady(const char *java_script); + + void Close(); + + void SetApplicationNameForUserAgent(const std::string &app_name); + + void OnLoadChanged(WebKitLoadEvent load_event); + + void GoBack(); + + void GoForward(); + + void Reload(); + + void StopLoading(); + + gboolean DecidePolicy(WebKitPolicyDecision *decision, + WebKitPolicyDecisionType type); + + void EvaluateJavaScript(const char *java_script, FlMethodCall* call); + + private: + FlMethodChannel *method_channel_; + int64_t window_id_; + std::function on_close_callback_; + + std::string default_user_agent_; + + GtkWidget *window_ = nullptr; + GtkWidget *webview_ = nullptr; + GtkBox *box_ = nullptr; + +}; + +#endif //WEBVIEW_WINDOW_LINUX_WEBVIEW_WINDOW_H_ diff --git a/packages/desktop_webview_window/pubspec.lock b/packages/desktop_webview_window/pubspec.lock new file mode 100644 index 00000000..891bb67a --- /dev/null +++ b/packages/desktop_webview_window/pubspec.lock @@ -0,0 +1,181 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" + source: hosted + version: "1.10.0" + path: + dependency: "direct main" + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" +sdks: + dart: ">=3.2.0-194.0.dev <4.0.0" + flutter: ">=2.5.0" diff --git a/packages/desktop_webview_window/pubspec.yaml b/packages/desktop_webview_window/pubspec.yaml new file mode 100644 index 00000000..16088c62 --- /dev/null +++ b/packages/desktop_webview_window/pubspec.yaml @@ -0,0 +1,28 @@ +name: desktop_webview_window +description: Show a webview window on your flutter desktop application. +version: 0.2.3 +homepage: https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_webview_window + +environment: + sdk: ">=2.14.0 <4.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + path: ^1.8.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + plugin: + platforms: + linux: + pluginClass: DesktopWebviewWindowPlugin diff --git a/packages/desktop_webview_window/run_local_test_server.sh b/packages/desktop_webview_window/run_local_test_server.sh new file mode 100644 index 00000000..b09d9afd --- /dev/null +++ b/packages/desktop_webview_window/run_local_test_server.sh @@ -0,0 +1 @@ +python3 -m http.server 3000 --directory example/test_web_pages \ No newline at end of file diff --git a/packages/desktop_webview_window/test/desktop_webview_window_test.dart b/packages/desktop_webview_window/test/desktop_webview_window_test.dart new file mode 100644 index 00000000..ab73b3a2 --- /dev/null +++ b/packages/desktop_webview_window/test/desktop_webview_window_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/pubspec.lock b/pubspec.lock index 865ce0a0..94d69b8f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -325,10 +325,9 @@ packages: desktop_webview_window: dependency: "direct main" description: - name: desktop_webview_window - sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" - url: "https://pub.dev" - source: hosted + path: "packages/desktop_webview_window" + relative: true + source: path version: "0.2.3" directed_graph: dependency: transitive @@ -483,10 +482,58 @@ packages: dependency: "direct main" description: name: flutter_inappwebview - sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350 + sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" url: "https://pub.dev" source: hosted - version: "5.8.0" + version: "6.0.0" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + url: "https://pub.dev" + source: hosted + version: "1.0.8" flutter_launcher_icons: dependency: "direct dev" description: @@ -550,6 +597,15 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_windows_webview: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: "096d24d88454819b9f8a94af14a130cb372fa5ae" + url: "https://github.com/wgh136/flutter_windows_webview" + source: git + version: "0.0.1" font_awesome_flutter: dependency: "direct main" description: @@ -614,6 +670,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.2" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" html: dependency: "direct main" description: @@ -1271,6 +1335,62 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" shelf: dependency: transitive description: @@ -1572,6 +1692,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + webf: + dependency: "direct main" + description: + name: webf + sha256: "55e69ad0d56c79f33c9c1ac011051d20f2b2d7722542a729119586b40648381e" + url: "https://pub.dev" + source: hosted + version: "0.16.0-beta.3" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 959651ff..97ce38bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,13 +33,14 @@ dependencies: url: https://github.com/kodjodevf/background_downloader.git ref: 2cc16eeb475788ec0443b64badc325c00d7dab90 permission_handler: ^11.2.0 - flutter_inappwebview: ^5.8.0 + flutter_inappwebview: ^6.0.0 draggable_menu: ^4.4.1 isar: 3.1.0+1 isar_flutter_libs: 3.1.0+1 share_plus: ^7.2.2 xpath_selector_html_parser: ^3.0.1 - desktop_webview_window: ^0.2.3 + desktop_webview_window: + path: ./packages/desktop_webview_window archive: ^3.4.10 file_picker: ^6.1.1 path_provider: ^2.1.2 @@ -68,6 +69,11 @@ dependencies: http_interceptor: ^2.0.0-beta.7 cookie_jar: ^4.0.0 http: ^1.2.0 + flutter_windows_webview: + git: + url: https://github.com/wgh136/flutter_windows_webview + ref: master + webf: ^0.16.0-beta.3 dependency_overrides: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 7eaad86c..90c87b1f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,7 +6,7 @@ #include "generated_plugin_registrant.h" -#include +#include #include #include #include @@ -15,12 +15,13 @@ #include #include #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - DesktopWebviewWindowPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); + FlutterWindowsWebviewPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterWindowsWebviewPluginCApi")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( @@ -37,6 +38,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WebfPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WebfPlugin")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); WindowToFrontPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 594d627f..b8215e50 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - desktop_webview_window + flutter_windows_webview isar_flutter_libs media_kit_libs_windows_video media_kit_video @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever share_plus url_launcher_windows + webf window_manager window_to_front )