updated web support

This commit is contained in:
Lorenzo Pichilli 2022-04-24 04:50:44 +02:00
parent 61a439893b
commit d0657c3b9a
7 changed files with 262 additions and 83 deletions

View File

@ -23,7 +23,8 @@ class _InAppWebViewExampleScreenState extends State<InAppWebViewExampleScreen> {
useHybridComposition: true,
allowsInlineMediaPlayback: true,
iframeAllow: "camera; microphone",
iframeAllowFullscreen: true
iframeAllowFullscreen: true,
javaScriptCanOpenWindowsAutomatically: false
);
PullToRefreshController? pullToRefreshController;

View File

@ -28,6 +28,7 @@
window.addEventListener('load', function (event) {
setTimeout(function () {
console.log('test');
window.open('https://google.com');
});
});
</script>

View File

@ -6,7 +6,8 @@ window.flutter_inappwebview = {
windows: {},
isFullscreen: false,
documentTitle: null,
prepare: function () {
functionMap: {},
prepare: function (settings) {
var iframe = document.getElementById(window.flutter_inappwebview.iframeId);
document.addEventListener('fullscreenchange', function(event) {
@ -30,6 +31,14 @@ window.flutter_inappwebview = {
window.flutter_inappwebview.windowAutoincrementId = 0;
window.flutter_inappwebview.windows = {};
var url = iframe.src;
try {
url = iframe.contentWindow.location.href;
} catch (e) {
console.log(e);
}
window.flutter_inappwebview.nativeCommunication('onLoadStart', window.flutter_inappwebview.viewId, [url]);
try {
var oldLogs = {
'log': iframe.contentWindow.console.log,
@ -58,24 +67,6 @@ window.flutter_inappwebview = {
console.log(e);
}
var url = iframe.src;
try {
url = iframe.contentWindow.location.href;
} catch (e) {
console.log(e);
}
window.flutter_inappwebview.nativeCommunication('onLoadStart', window.flutter_inappwebview.viewId, [url]);
window.flutter_inappwebview.nativeCommunication('onLoadStop', window.flutter_inappwebview.viewId, [url]);
iframe.contentWindow.addEventListener('popstate', function (event) {
var iframeUrl = iframe.src;
try {
iframeUrl = iframe.contentWindow.location.href;
} catch (e) {
console.log(e);
}
window.flutter_inappwebview.nativeCommunication('onUpdateVisitedHistory', window.flutter_inappwebview.viewId, [iframeUrl]);
});
try {
var originalPushState = iframe.contentWindow.history.pushState;
iframe.contentWindow.history.pushState = function (state, unused, url) {
@ -88,6 +79,7 @@ window.flutter_inappwebview = {
}
window.flutter_inappwebview.nativeCommunication('onUpdateVisitedHistory', window.flutter_inappwebview.viewId, [iframeUrl]);
};
var originalReplaceState = iframe.contentWindow.history.replaceState;
iframe.contentWindow.history.replaceState = function (state, unused, url) {
originalReplaceState.call(iframe.contentWindow.history, state, unused, url);
@ -99,11 +91,7 @@ window.flutter_inappwebview = {
}
window.flutter_inappwebview.nativeCommunication('onUpdateVisitedHistory', window.flutter_inappwebview.viewId, [iframeUrl]);
};
} catch (e) {
console.log(e);
}
try {
var originalOpen = iframe.contentWindow.open;
iframe.contentWindow.open = function (url, target, windowFeatures) {
var newWindow = originalOpen.call(iframe.contentWindow, ...arguments);
@ -111,18 +99,13 @@ window.flutter_inappwebview = {
window.flutter_inappwebview.windowAutoincrementId++;
window.flutter_inappwebview.windows[windowId] = newWindow;
window.flutter_inappwebview.nativeCommunication('onCreateWindow', window.flutter_inappwebview.viewId, [windowId, url, target, windowFeatures]).then(function(){}, function(handledByClient) {
console.log(handledByClient);
if (handledByClient) {
newWindow.close();
}
});
return newWindow;
};
} catch (e) {
console.log(e);
}
try {
var originalPrint = iframe.contentWindow.print;
iframe.contentWindow.print = function () {
var iframeUrl = iframe.src;
@ -134,31 +117,14 @@ window.flutter_inappwebview = {
window.flutter_inappwebview.nativeCommunication('onPrint', window.flutter_inappwebview.viewId, [iframeUrl]);
originalPrint.call(iframe.contentWindow);
};
} catch (e) {
console.log(e);
}
iframe.contentWindow.addEventListener('scroll', function (event) {
var x = 0;
var y = 0;
try {
x = iframe.contentWindow.scrollX;
y = iframe.contentWindow.scrollY;
} catch (e) {
console.log(e);
window.flutter_inappwebview.functionMap = {
"window.open": iframe.contentWindow.open,
"window.print": iframe.contentWindow.print,
"window.history.pushState": iframe.contentWindow.history.pushState,
"window.history.replaceState": iframe.contentWindow.history.replaceState,
}
window.flutter_inappwebview.nativeCommunication('onScrollChanged', window.flutter_inappwebview.viewId, [x, y]);
});
iframe.contentWindow.addEventListener('focus', function (event) {
window.flutter_inappwebview.nativeCommunication('onWindowFocus', window.flutter_inappwebview.viewId);
});
iframe.contentWindow.addEventListener('blur', function (event) {
window.flutter_inappwebview.nativeCommunication('onWindowBlur', window.flutter_inappwebview.viewId);
});
try {
var initialTitle = iframe.contentDocument.title;
window.flutter_inappwebview.documentTitle = initialTitle;
window.flutter_inappwebview.nativeCommunication('onTitleChanged', window.flutter_inappwebview.viewId, [initialTitle]);
@ -172,11 +138,7 @@ window.flutter_inappwebview = {
iframe.contentDocument.querySelector('title'),
{ subtree: true, characterData: true, childList: true }
);
} catch (e) {
console.log(e);
}
try {
var oldPixelRatio = iframe.contentWindow.devicePixelRatio;
iframe.contentWindow.addEventListener('resize', function (e) {
var newPixelRatio = iframe.contentWindow.devicePixelRatio;
@ -185,12 +147,70 @@ window.flutter_inappwebview = {
oldPixelRatio = newPixelRatio;
}
});
iframe.contentWindow.addEventListener('popstate', function (event) {
var iframeUrl = iframe.src;
try {
iframeUrl = iframe.contentWindow.location.href;
} catch (e) {
console.log(e);
}
window.flutter_inappwebview.nativeCommunication('onUpdateVisitedHistory', window.flutter_inappwebview.viewId, [iframeUrl]);
});
iframe.contentWindow.addEventListener('scroll', function (event) {
var x = 0;
var y = 0;
try {
x = iframe.contentWindow.scrollX;
y = iframe.contentWindow.scrollY;
} catch (e) {
console.log(e);
}
window.flutter_inappwebview.nativeCommunication('onScrollChanged', window.flutter_inappwebview.viewId, [x, y]);
});
iframe.contentWindow.addEventListener('focus', function (event) {
window.flutter_inappwebview.nativeCommunication('onWindowFocus', window.flutter_inappwebview.viewId);
});
iframe.contentWindow.addEventListener('blur', function (event) {
window.flutter_inappwebview.nativeCommunication('onWindowBlur', window.flutter_inappwebview.viewId);
});
} catch (e) {
console.log(e);
}
try {
if (!settings.javaScriptCanOpenWindowsAutomatically) {
iframe.contentWindow.open = function () {
throw new Error('JavaScript cannot open windows automatically');
};
}
} catch (e) {
console.log(e);
}
window.flutter_inappwebview.nativeCommunication('onLoadStop', window.flutter_inappwebview.viewId, [url]);
});
}
},
setSettings: function (settings, newSettings) {
var iframe = window.flutter_inappwebview.iframe;
try {
if (settings.javaScriptCanOpenWindowsAutomatically != newSettings.javaScriptCanOpenWindowsAutomatically) {
if (!newSettings.javaScriptCanOpenWindowsAutomatically) {
iframe.contentWindow.open = function () {
throw new Error('JavaScript cannot open windows automatically');
};
} else {
iframe.contentWindow.open = window.flutter_inappwebview.functionMap["window.open"];
}
}
} catch (e) {
console.log(e);
}
},
reload: function () {
var iframe = window.flutter_inappwebview.iframe;
if (iframe != null && iframe.contentWindow != null) {
@ -237,10 +257,8 @@ window.flutter_inappwebview = {
var result = null;
if (iframe != null) {
try {
result = iframe.contentWindow.eval(source);
} catch (e) {
console.log(e);
}
result = JSON.stringify(iframe.contentWindow.eval(source));
} catch (e) {}
}
return result;
},

View File

@ -1552,7 +1552,7 @@ class InAppWebViewController
///Reloads the WebView.
///
///**NOTE**: on Web, if `window.location.reload()` is not accessible inside the iframe, it will reload using the iframe `src` attribute.
///**NOTE for Web**: if `window.location.reload()` is not accessible inside the iframe, it will reload using the iframe `src` attribute.
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - WebView.reload](https://developer.android.com/reference/android/webkit/WebView#reload()))
@ -1565,6 +1565,8 @@ class InAppWebViewController
///Goes back in the history of the WebView.
///
///**NOTE for Web**: this method will have effect only if the iframe has the same origin.
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - WebView.goBack](https://developer.android.com/reference/android/webkit/WebView#goBack()))
///- iOS ([Official API - WKWebView.goBack](https://developer.apple.com/documentation/webkit/wkwebview/1414952-goback))
@ -1586,6 +1588,8 @@ class InAppWebViewController
///Goes forward in the history of the WebView.
///
///**NOTE for Web**: this method will have effect only if the iframe has the same origin.
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - WebView.goForward](https://developer.android.com/reference/android/webkit/WebView#goForward()))
///- iOS ([Official API - WKWebView.goForward](https://developer.apple.com/documentation/webkit/wkwebview/1414993-goforward))
@ -1607,6 +1611,8 @@ class InAppWebViewController
///Goes to the history item that is the number of steps away from the current item. Steps is negative if backward and positive if forward.
///
///**NOTE for Web**: this method will have effect only if the iframe has the same origin.
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - WebView.goBackOrForward](https://developer.android.com/reference/android/webkit/WebView#goBackOrForward(int)))
///- iOS ([Official API - WKWebView.go](https://developer.apple.com/documentation/webkit/wkwebview/1414991-go))
@ -1630,6 +1636,8 @@ class InAppWebViewController
///Navigates to a [WebHistoryItem] from the back-forward [WebHistory.list] and sets it as the current item.
///
///**NOTE for Web**: this method will have effect only if the iframe has the same origin.
///
///**Supported Platforms/Implementations**:
///- Android native WebView
///- iOS
@ -1654,9 +1662,12 @@ class InAppWebViewController
///Stops the WebView from loading.
///
///**NOTE for Web**: this method will have effect only if the iframe has the same origin.
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - WebView.stopLoading](https://developer.android.com/reference/android/webkit/WebView#stopLoading()))
///- iOS ([Official API - WKWebView.stopLoading](https://developer.apple.com/documentation/webkit/wkwebview/1414981-stoploading))
///- Web ([Official API - Window.stop](https://developer.mozilla.org/en-US/docs/Web/API/Window/stop))
Future<void> stopLoading() async {
Map<String, dynamic> args = <String, dynamic>{};
await _channel.invokeMethod('stopLoading', args);
@ -1677,6 +1688,8 @@ class InAppWebViewController
///Instead, you should call this method, for example, inside the [WebView.onLoadStop] event or in any other events
///where you know the page is ready "enough".
///
///**NOTE for Web**: this method will have effect only if the iframe has the same origin.
///
///**Supported Platforms/Implementations**:
///- Android native WebView ([Official API - WebView.evaluateJavascript](https://developer.android.com/reference/android/webkit/WebView#evaluateJavascript(java.lang.String,%20android.webkit.ValueCallback%3Cjava.lang.String%3E)))
///- iOS ([Official API - WKWebView.evaluateJavascript](https://developer.apple.com/documentation/webkit/wkwebview/3656442-evaluatejavascript))
@ -1687,7 +1700,7 @@ class InAppWebViewController
args.putIfAbsent('source', () => source);
args.putIfAbsent('contentWorld', () => contentWorld?.toMap());
var data = await _channel.invokeMethod('evaluateJavascript', args);
if (data != null && defaultTargetPlatform == TargetPlatform.android) {
if (data != null && (defaultTargetPlatform == TargetPlatform.android || kIsWeb)) {
try {
// try to json decode the data coming from JavaScript
// otherwise return it as it is.

View File

@ -87,13 +87,17 @@ class InAppWebViewSettings
///**Supported Platforms/Implementations**:
///- Android native WebView
///- iOS
///- Web
bool javaScriptEnabled;
///Set to `true` to allow JavaScript open windows without user interaction. The default value is `false`.
///
///**NOTE for Web**: this setting will have effect only if the iframe has the same origin.
///
///**Supported Platforms/Implementations**:
///- Android native WebView
///- iOS
///- Web
bool javaScriptCanOpenWindowsAutomatically;
///Set to `true` to prevent HTML5 audio or video from autoplaying. The default value is `true`.
@ -995,31 +999,31 @@ class InAppWebViewSettings
///- iOS
bool upgradeKnownHostsToHTTPS;
///Specifies a feature policy for the iframe. A list of origins the frame is allowed to display content from.
///This attribute also accepts the values `self` and `src` which represent the origin in the iframe's src attribute.
///The default value is `src`.
///Specifies a feature policy for the `<iframe>`.
///The policy defines what features are available to the `<iframe>` based on the origin of the request
///(e.g. access to the microphone, camera, battery, web-share API, etc.).
///
///**Supported Platforms/Implementations**:
///- Web
String? iframeAllow;
///A boolean value indicating whether the inline frame is willing to be placed into full screen mode.
///Set to true if the `<iframe>` can activate fullscreen mode by calling the `requestFullscreen()` method.
///
///**Supported Platforms/Implementations**:
///- Web
bool? iframeAllowFullscreen;
///A DOMTokenList that reflects the sandbox HTML attribute, indicating extra restrictions on the behavior of the nested content.
///Applies extra restrictions to the content in the frame.
///
///**Supported Platforms/Implementations**:
///- Web
String? iframeSandox;
List<Sandbox>? iframeSandbox;
///A string that reflects the `referrerpolicy` HTML attribute indicating which referrer to use when fetching the linked resource.
///
///**Supported Platforms/Implementations**:
///- Web
String? iframeReferrerPolicy;
ReferrerPolicy? iframeReferrerPolicy;
///A string that reflects the `name` HTML attribute, containing a name by which to refer to the frame.
///
@ -1027,7 +1031,7 @@ class InAppWebViewSettings
///- Web
String? iframeName;
///Specifies the Content Security Policy that an embedded document must agree to enforce upon itself.
///A Content Security Policy enforced for the embedded resource.
///
///**Supported Platforms/Implementations**:
///- Web
@ -1155,7 +1159,7 @@ class InAppWebViewSettings
this.upgradeKnownHostsToHTTPS = true,
this.iframeAllow,
this.iframeAllowFullscreen,
this.iframeSandox,
this.iframeSandbox,
this.iframeReferrerPolicy,
this.iframeName,
this.iframeCsp,}) {
@ -1303,7 +1307,7 @@ class InAppWebViewSettings
"upgradeKnownHostsToHTTPS": upgradeKnownHostsToHTTPS,
"iframeAllow": iframeAllow,
"iframeAllowFullscreen": iframeAllowFullscreen,
"iframeSandox": iframeSandox,
"iframeSandbox": iframeSandbox?.map((e) => e.toValue()),
"iframeReferrerPolicy": iframeReferrerPolicy,
"iframeName": iframeName,
"iframeCsp": iframeCsp,
@ -1366,8 +1370,9 @@ class InAppWebViewSettings
if (kIsWeb) {
settings.iframeAllow = map["iframeAllow"];
settings.iframeAllowFullscreen = map["iframeAllowFullscreen"];
settings.iframeSandox = map["iframeSandox"];
settings.iframeReferrerPolicy = map["iframeReferrerPolicy"];
settings.iframeSandbox = (map["iframeSandbox"] as List<String?>?)
?.map((e) => Sandbox.fromValue(e)) as List<Sandbox>?;
settings.iframeReferrerPolicy = ReferrerPolicy.fromValue(map["iframeReferrerPolicy"]);
settings.iframeName = map["iframeName"];
settings.iframeCsp = map["iframeCsp"];
}

View File

@ -10724,3 +10724,97 @@ class MediaPlaybackState {
@override
int get hashCode => _value.hashCode;
}
///Class that describes what to allow in the iframe.
class Sandbox {
final String? _value;
const Sandbox._internal(this._value);
static final Set<Sandbox> values = [
Sandbox.ALLOW_DOWNLOADS,
Sandbox.ALLOW_FORMS,
Sandbox.ALLOW_MODALS,
Sandbox.ALLOW_ORIENTATION_LOCK,
Sandbox.ALLOW_POINTER_LOCK,
Sandbox.ALLOW_POPUPS,
Sandbox.ALLOW_POPUPS_TO_ESCAPE_SANDBOX,
Sandbox.ALLOW_PRESENTATION,
Sandbox.ALLOW_SAME_ORIGIN,
Sandbox.ALLOW_SCRIPTS,
Sandbox.ALLOW_TOP_NAVIGATION,
Sandbox.ALLOW_TOP_NAVIGATION_BY_USER_ACTIVATION,
].toSet();
static Sandbox? fromValue(String? value) {
if (value == null) {
return _ALL;
} else if (value == "") {
return _NONE;
}
try {
return Sandbox.values
.firstWhere((element) => element.toValue() == value);
} catch (e) {
return null;
}
}
String? toValue() => _value;
@override
String toString() => _value == null ? "allow-all" : (_value == "" ? "allow-none" : "");
static const _ALL = const Sandbox._internal(null);
static const _NONE = const Sandbox._internal("");
///Allow all.
static const ALLOW_ALL = const [_ALL];
///Allow none.
static const ALLOW_NONE = const [_NONE];
///Allows for downloads to occur with a gesture from the user.
static const ALLOW_DOWNLOADS = const Sandbox._internal("allow-downloads");
///Allows the resource to submit forms. If this keyword is not used, form submission is blocked.
static const ALLOW_FORMS = const Sandbox._internal("allow-forms");
///Lets the resource open modal windows.
static const ALLOW_MODALS = const Sandbox._internal("allow-modals");
///Lets the resource lock the screen orientation.
static const ALLOW_ORIENTATION_LOCK = const Sandbox._internal("allow-orientation-lock");
///Lets the resource use the Pointer Lock API.
static const ALLOW_POINTER_LOCK = const Sandbox._internal("allow-pointer-lock");
///Allows popups (such as `window.open()`, `target="_blank"`, or `showModalDialog()`).
///If this keyword is not used, the popup will silently fail to open.
static const ALLOW_POPUPS = const Sandbox._internal("allow-popups");
///Lets the sandboxed document open new windows without those windows inheriting the sandboxing.
///For example, this can safely sandbox an advertisement without forcing the same restrictions upon the page the ad links to.
static const ALLOW_POPUPS_TO_ESCAPE_SANDBOX = const Sandbox._internal("allow-popups-to-escape-sandbox");
///Lets the resource start a presentation session.
static const ALLOW_PRESENTATION = const Sandbox._internal("allow-presentation");
///If this token is not used, the resource is treated as being from a special origin that always fails the
///same-origin policy (potentially preventing access to data storage/cookies and some JavaScript APIs).
static const ALLOW_SAME_ORIGIN = const Sandbox._internal("allow-same-origin");
///Lets the resource run scripts (but not create popup windows).
static const ALLOW_SCRIPTS = const Sandbox._internal("allow-scripts");
///Lets the resource navigate the top-level browsing context (the one named `_top`).
static const ALLOW_TOP_NAVIGATION = const Sandbox._internal("allow-top-navigation");
///Lets the resource navigate the top-level browsing context, but only if initiated by a user gesture.
static const ALLOW_TOP_NAVIGATION_BY_USER_ACTIVATION = const Sandbox._internal("allow-top-navigation-by-user-activation");
bool operator ==(value) => value == _value;
@override
int get hashCode => _value.hashCode;
}

View File

@ -97,16 +97,28 @@ class InAppWebViewWebElement {
void prepare() {
settings = initialSettings ?? InAppWebViewSettings();
Set<Sandbox> sandbox = Set.from(Sandbox.values);
if (!settings.javaScriptEnabled) {
sandbox.remove(Sandbox.ALLOW_SCRIPTS);
}
iframe.allow = settings.iframeAllow ?? iframe.allow;
iframe.allowFullscreen = settings.iframeAllowFullscreen ?? iframe.allowFullscreen;
if (settings.iframeSandox != null) {
iframe.setAttribute("sandbox", settings.iframeSandox ?? "");
}
iframe.referrerPolicy = settings.iframeReferrerPolicy ?? iframe.referrerPolicy;
iframe.referrerPolicy = settings.iframeReferrerPolicy?.toValue() ?? iframe.referrerPolicy;
iframe.name = settings.iframeName ?? iframe.name;
iframe.csp = settings.iframeCsp ?? iframe.csp;
bridgeJsObject.callMethod("prepare");
if (settings.iframeSandbox != null && settings.iframeSandbox != Sandbox.ALLOW_ALL) {
iframe.setAttribute("sandbox", settings.iframeSandbox!.map((e) => e.toValue()).join(" "));
} else if (settings.iframeSandbox == Sandbox.ALLOW_ALL) {
iframe.removeAttribute("sandbox");
} else if (sandbox != Sandbox.values) {
iframe.setAttribute("sandbox", sandbox.map((e) => e.toValue()).join(" "));
}
bridgeJsObject.callMethod("prepare", [js.JsObject.jsify(settings.toMap())]);
}
void makeInitialLoad() async {
@ -179,18 +191,39 @@ class InAppWebViewWebElement {
bridgeJsObject.callMethod("stopLoading");
}
Set<Sandbox> getSandbox() {
var sandbox = iframe.sandbox;
Set<Sandbox> values = Set();
if (sandbox != null) {
for (int i = 0; i < sandbox.length; i++) {
var token = Sandbox.fromValue(sandbox.item(i));
if (token != null) {
values.add(token);
}
}
}
return values.isEmpty ? Set.from(Sandbox.values) : values;
}
Future<void> setSettings(InAppWebViewSettings newSettings) async {
Set<Sandbox> sandbox = getSandbox();
if (settings.javaScriptEnabled != newSettings.javaScriptEnabled) {
if (!newSettings.javaScriptEnabled) {
sandbox.remove(Sandbox.ALLOW_SCRIPTS);
} else {
sandbox.add(Sandbox.ALLOW_SCRIPTS);
}
}
if (settings.iframeAllow != newSettings.iframeAllow) {
iframe.allow = newSettings.iframeAllow;
}
if (settings.iframeAllowFullscreen != newSettings.iframeAllowFullscreen) {
iframe.allowFullscreen = newSettings.iframeAllowFullscreen;
}
if (settings.iframeSandox != newSettings.iframeSandox) {
iframe.setAttribute("sandbox", newSettings.iframeSandox ?? "");
}
if (settings.iframeReferrerPolicy != newSettings.iframeReferrerPolicy) {
iframe.referrerPolicy = newSettings.iframeReferrerPolicy;
iframe.referrerPolicy = newSettings.iframeReferrerPolicy?.toValue();
}
if (settings.iframeName != newSettings.iframeName) {
iframe.name = newSettings.iframeName;
@ -198,6 +231,20 @@ class InAppWebViewWebElement {
if (settings.iframeCsp != newSettings.iframeCsp) {
iframe.csp = newSettings.iframeCsp;
}
if (settings.iframeSandbox != newSettings.iframeSandbox) {
var sandbox = newSettings.iframeSandbox;
if (sandbox != null && sandbox != Sandbox.ALLOW_ALL) {
iframe.setAttribute("sandbox", sandbox.map((e) => e.toValue()).join(" "));
} else if (sandbox == Sandbox.ALLOW_ALL) {
iframe.removeAttribute("sandbox");
}
} else if (sandbox != Sandbox.values) {
iframe.setAttribute("sandbox", sandbox.map((e) => e.toValue()).join(" "));
}
bridgeJsObject.callMethod("setSettings", [js.JsObject.jsify(settings.toMap()), js.JsObject.jsify(newSettings.toMap())]);
settings = newSettings;
}
@ -326,4 +373,4 @@ class InAppWebViewWebElement {
await _channel.invokeMethod("onZoomScaleChanged", obj);
}
}
}