222 lines
8.0 KiB
Dart
222 lines
8.0 KiB
Dart
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<bool> {
|
|
static final WebviewManager _instance = WebviewManager._internal();
|
|
|
|
factory WebviewManager() => _instance;
|
|
|
|
late Completer<void> _creatingCompleter;
|
|
|
|
final MethodChannel pluginChannel = const MethodChannel("webview_cef");
|
|
|
|
final _webViews = <int, WebViewController>{};
|
|
final _injectUserScripts = <int, InjectUserScripts?>{};
|
|
|
|
final _tempWebViews = <int, WebViewController>{};
|
|
final _tempInjectUserScripts = <int, InjectUserScripts?>{};
|
|
|
|
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<bool> get hasNativeKeySupport async {
|
|
_hasNativeKeySupport ??= await pluginChannel.invokeMethod<bool>('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<void> initialize({String? userAgent, String? rootCachePath, String? proxyServer, bool doNotDeElevate = false}) async {
|
|
_creatingCompleter = Completer<void>();
|
|
try {
|
|
final Map<String, String> settings = <String, String>{
|
|
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<void> 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<void> 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<void> _injectUserScriptIfNeeds(int browserId, List<UserScript> scripts) async {
|
|
if (scripts.isEmpty) return;
|
|
|
|
await _webViews[browserId]?.ready;
|
|
|
|
for (final script in scripts) {
|
|
await _webViews[browserId]?.executeJavaScript(script.script);
|
|
}
|
|
}
|
|
|
|
Future<void> setCookie(String domain, String key, String val) async {
|
|
assert(value);
|
|
return pluginChannel.invokeMethod('setCookie', [domain, key, val]);
|
|
}
|
|
|
|
Future<void> deleteCookie(String domain, String key) async {
|
|
assert(value);
|
|
return pluginChannel.invokeMethod('deleteCookie', [domain, key]);
|
|
}
|
|
|
|
Future<dynamic> visitAllCookies() async {
|
|
assert(value);
|
|
return pluginChannel.invokeMethod('visitAllCookies');
|
|
}
|
|
|
|
Future<dynamic> visitUrlCookies(String domain, bool isHttpOnly) async {
|
|
assert(value);
|
|
return pluginChannel.invokeMethod('visitUrlCookies', [domain, isHttpOnly]);
|
|
}
|
|
|
|
Future<void> quit() async {
|
|
//only call this method when you want to quit the app
|
|
assert(value);
|
|
return pluginChannel.invokeMethod('quit');
|
|
}
|
|
}
|