import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:webview_cef/src/webview_inject_user_script.dart'; import 'webview.dart'; class WebviewManager extends ValueNotifier { static final WebviewManager _instance = WebviewManager._internal(); factory WebviewManager() => _instance; late Completer _creatingCompleter; final MethodChannel pluginChannel = const MethodChannel("webview_cef"); final _webViews = {}; final _injectUserScripts = {}; final _tempWebViews = {}; final _tempInjectUserScripts = {}; int nextIndex = 1; bool? _hasNativeKeySupport; get ready => _creatingCompleter.future; /// Returns true if the platform has native key event handling (e.g., GTK on desktop Linux). /// When false, Dart-side key handling should be used (e.g., eLinux). Future get hasNativeKeySupport async { _hasNativeKeySupport ??= await pluginChannel.invokeMethod('hasNativeKeySupport') ?? false; return _hasNativeKeySupport!; } WebViewController createWebView({ Widget? loading, InjectUserScripts? injectUserScripts, }) { int browserIndex = nextIndex++; final controller = WebViewController(pluginChannel, browserIndex, loading: loading); _tempWebViews[browserIndex] = controller; _tempInjectUserScripts[browserIndex] = injectUserScripts; return controller; } void removeWebView(int browserId) { if (browserId > 0) { _webViews.remove(browserId); } } WebviewManager._internal() : super(false); /// Initializes the CEF runtime. /// /// [rootCachePath] is the directory CEF stores its user data in. Leaving it out makes CEF use /// the platform default, where it also keeps the process singleton lock that lets a second /// instance hand its request to the first one and exit. CEF only accepts an absolute path, a /// relative one is resolved against the working directory of the process. /// /// [proxyServer] (JJ_MKVMaker, JJ_PATCH.md 6): every connection goes through this proxy (e.g. `http://127.0.0.1:1234`). /// [doNotDeElevate] (JJ_MKVMaker, JJ_PATCH.md 7): keep running when the process is elevated (Chromium otherwise relaunches de-elevated and exits). Future initialize({String? userAgent, String? rootCachePath, String? proxyServer, bool doNotDeElevate = false}) async { _creatingCompleter = Completer(); try { final Map settings = { if (userAgent != null && userAgent.isNotEmpty) 'userAgent': userAgent, if (rootCachePath != null && rootCachePath.isNotEmpty) 'rootCachePath': rootCachePath, if (proxyServer != null && proxyServer.isNotEmpty) 'proxyServer': proxyServer, if (doNotDeElevate) 'doNotDeElevate': '1', }; if (settings.isNotEmpty) { await pluginChannel.invokeMethod('init', settings); } else { await pluginChannel.invokeMethod('init'); } pluginChannel.setMethodCallHandler(methodCallhandler); // Wait for the platform to complete initialization. await Future.delayed(const Duration(milliseconds: 300)); _creatingCompleter.complete(); value = true; } on PlatformException catch (e) { _creatingCompleter.completeError(e); } return _creatingCompleter.future; } @override Future dispose() async { super.dispose(); pluginChannel.setMethodCallHandler(null); _webViews.clear(); } void onBrowserCreated(int browserIndex, int browserId) { _webViews[browserId] = _tempWebViews[browserIndex]!; _injectUserScripts[browserId] = _tempInjectUserScripts[browserIndex]; _tempWebViews.remove(browserIndex); _tempInjectUserScripts.remove(browserIndex); } Future methodCallhandler(MethodCall call) async { switch (call.method) { case "urlChanged": int browserId = call.arguments["browserId"] as int; _webViews[browserId] ?.listener ?.onUrlChanged ?.call(call.arguments["url"] as String); return; case "titleChanged": int browserId = call.arguments["browserId"] as int; _webViews[browserId] ?.listener ?.onTitleChanged ?.call(call.arguments["title"] as String); return; case "onConsoleMessage": int browserId = call.arguments["browserId"] as int; _webViews[browserId]?.listener?.onConsoleMessage?.call( call.arguments["level"] as int, call.arguments["message"] as String, call.arguments["source"] as String, call.arguments["line"] as int); return; case 'javascriptChannelMessage': int browserId = call.arguments['browserId'] as int; _webViews[browserId]?.onJavascriptChannelMessage?.call( call.arguments['channel'] as String, call.arguments['message'] as String, call.arguments['callbackId'] as String, call.arguments['frameId'] as String); return; case 'onTooltip': int browserId = call.arguments['browserId'] as int; _webViews[browserId]?.onToolTip?.call(call.arguments['text'] as String); return; case 'onCursorChanged': int browserId = call.arguments['browserId'] as int; _webViews[browserId] ?.onCursorChanged ?.call(call.arguments['type'] as int); return; case 'onFocusedNodeChangeMessage': int browserId = call.arguments['browserId'] as int; bool editable = call.arguments['editable'] as bool; _webViews[browserId]?.onFocusedNodeChangeMessage(editable); return; case 'onImeCompositionRangeChangedMessage': int browserId = call.arguments['browserId'] as int; _webViews[browserId]?.onImeCompositionRangeChangedMessage?.call( call.arguments['x'] as int, call.arguments['y'] as int, call.arguments['height'] as int); return; case 'onLoadStart': int browserId = call.arguments["browserId"] as int; String urlId = call.arguments["urlId"] as String; await _injectUserScriptIfNeeds(browserId, _injectUserScripts[browserId]?.retrieveLoadStartInjectScripts() ?? []); WebViewController controller = _webViews[browserId] as WebViewController; _webViews[browserId]?.listener?.onLoadStart?.call(controller, urlId); return; case 'onLoadEnd': int browserId = call.arguments["browserId"] as int; String urlId = call.arguments["urlId"] as String; await _injectUserScriptIfNeeds(browserId, _injectUserScripts[browserId]?.retrieveLoadEndInjectScripts() ?? []); WebViewController controller = _webViews[browserId] as WebViewController; _webViews[browserId]?.listener?.onLoadEnd?.call(controller, urlId); return; default: } } Future _injectUserScriptIfNeeds(int browserId, List scripts) async { if (scripts.isEmpty) return; await _webViews[browserId]?.ready; for (final script in scripts) { await _webViews[browserId]?.executeJavaScript(script.script); } } Future setCookie(String domain, String key, String val) async { assert(value); return pluginChannel.invokeMethod('setCookie', [domain, key, val]); } Future deleteCookie(String domain, String key) async { assert(value); return pluginChannel.invokeMethod('deleteCookie', [domain, key]); } Future visitAllCookies() async { assert(value); return pluginChannel.invokeMethod('visitAllCookies'); } Future visitUrlCookies(String domain, bool isHttpOnly) async { assert(value); return pluginChannel.invokeMethod('visitUrlCookies', [domain, isHttpOnly]); } Future quit() async { //only call this method when you want to quit the app assert(value); return pluginChannel.invokeMethod('quit'); } }