adding packages
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export 'src/io.dart' if (dart.library.html) 'src/web.dart';
|
||||
export 'src/platforms/base.dart';
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'platforms/base.dart';
|
||||
import 'platforms/markdown.dart';
|
||||
import 'platforms/widgets.dart';
|
||||
|
||||
abstract class EasyWebViewBase extends StatelessWidget {
|
||||
const EasyWebViewBase({
|
||||
Key? key,
|
||||
required this.src,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.onLoaded,
|
||||
required this.isMarkdown,
|
||||
required this.convertToMarkdown,
|
||||
required this.convertToWidgets,
|
||||
required this.fallbackBuilder,
|
||||
required this.options,
|
||||
}) : super(key: key);
|
||||
|
||||
final String src;
|
||||
final double? width, height;
|
||||
final OnLoaded? onLoaded;
|
||||
final bool isMarkdown;
|
||||
final bool convertToMarkdown;
|
||||
final bool convertToWidgets;
|
||||
final WidgetBuilder? fallbackBuilder;
|
||||
final WebViewOptions options;
|
||||
|
||||
bool canBuild() {
|
||||
if (convertToWidgets) return true;
|
||||
if (isMarkdown || convertToMarkdown) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (canBuild()) {
|
||||
if (convertToWidgets) {
|
||||
return WidgetsWebView(
|
||||
key: key,
|
||||
src: src,
|
||||
width: width,
|
||||
height: height,
|
||||
onLoaded: onLoaded,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
|
||||
if (isMarkdown || convertToMarkdown) {
|
||||
return MarkdownWebView(
|
||||
key: key,
|
||||
src: src,
|
||||
height: height,
|
||||
width: width,
|
||||
onLoaded: onLoaded,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (fallbackBuilder != null) {
|
||||
return fallbackBuilder!(context);
|
||||
} else {
|
||||
return Placeholder();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
extension StringUtils on String {
|
||||
bool get isValidHtml => this.contains('<html>') && this.contains('</html>');
|
||||
|
||||
bool get isValidUrl =>
|
||||
this.startsWith('https://') || this.startsWith('http://');
|
||||
|
||||
bool get isValidMarkdown => !isValidUrl && !isValidHtml;
|
||||
|
||||
String toMarkdown() {
|
||||
if (isValidHtml) return html2md.convert(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
String toHtml() {
|
||||
if (isValidMarkdown) return md.markdownToHtml(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
String toDataUrl() {
|
||||
if (!isValidUrl) {
|
||||
String _content = this;
|
||||
if (isValidMarkdown) _content = toHtml();
|
||||
if (isValidHtml) _content = toHtml().wrapHtml();
|
||||
return "data:text/html;charset=utf-8,${Uri.encodeComponent(_content)}";
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
String wrapHtml({
|
||||
String title = 'Document',
|
||||
String pad = ' ',
|
||||
}) {
|
||||
final sb = StringBuffer();
|
||||
final add = (String s) => sb.writeln(s);
|
||||
add('<!DOCTYPE html>');
|
||||
add('<html lang="en">');
|
||||
add('<head>');
|
||||
add('$pad<meta charset="UTF-8">');
|
||||
add('$pad<meta name="viewport" content="width=device-width, initial-scale=1.0">');
|
||||
add('$pad<title>${title}</title>');
|
||||
add('</head>');
|
||||
add('<body>');
|
||||
add(this);
|
||||
add('</body>');
|
||||
add('</html>');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'platforms/base.dart';
|
||||
import 'platforms/native.dart';
|
||||
import 'platforms/windows.dart';
|
||||
import 'base.dart';
|
||||
|
||||
class EasyWebView extends EasyWebViewBase {
|
||||
const EasyWebView({
|
||||
Key? key,
|
||||
required String src,
|
||||
double? height,
|
||||
double? width,
|
||||
OnLoaded? onLoaded,
|
||||
bool isMarkdown = false,
|
||||
bool convertToMarkdown = false,
|
||||
bool convertToWidgets = false,
|
||||
WidgetBuilder? fallbackBuilder,
|
||||
WebViewOptions options = const WebViewOptions(),
|
||||
}) : super(
|
||||
key: key,
|
||||
src: src,
|
||||
height: height,
|
||||
width: width,
|
||||
onLoaded: onLoaded,
|
||||
isMarkdown: isMarkdown,
|
||||
convertToMarkdown: convertToMarkdown,
|
||||
convertToWidgets: convertToWidgets,
|
||||
fallbackBuilder: fallbackBuilder,
|
||||
options: options,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!canBuild()) {
|
||||
if (Platform.isIOS || Platform.isMacOS || Platform.isAndroid) {
|
||||
return NativeWebView(
|
||||
key: key,
|
||||
src: src,
|
||||
width: width,
|
||||
height: height,
|
||||
onLoaded: onLoaded,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.isWindows) {
|
||||
return WindowsWebView(
|
||||
key: key,
|
||||
src: src,
|
||||
width: width,
|
||||
height: height,
|
||||
onLoaded: onLoaded,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
}
|
||||
return super.build(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:webview_windows/webview_windows.dart'
|
||||
show WebviewPopupWindowPolicy;
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'
|
||||
show
|
||||
WidgetFactory,
|
||||
CustomStylesBuilder,
|
||||
CustomWidgetBuilder,
|
||||
OnErrorBuilder,
|
||||
OnLoadingBuilder,
|
||||
ImageMetadata,
|
||||
RenderMode;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../extensions.dart';
|
||||
import '../widgets/optionally_sized_child.dart';
|
||||
|
||||
abstract class WebView extends StatefulWidget {
|
||||
const WebView({
|
||||
Key? key,
|
||||
required this.src,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.onLoaded,
|
||||
}) : super(key: key);
|
||||
|
||||
final String src;
|
||||
final double? width, height;
|
||||
final OnLoaded? onLoaded;
|
||||
}
|
||||
|
||||
class WebViewState<T extends WebView> extends State<T> {
|
||||
@override
|
||||
void didUpdateWidget(T oldWidget) {
|
||||
if (oldWidget.src != widget.src ||
|
||||
oldWidget.height != widget.height ||
|
||||
oldWidget.width != widget.width) {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OptionalSizedChild(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
builder: (context, size) => builder(context, size, widget.src),
|
||||
);
|
||||
}
|
||||
|
||||
Widget builder(BuildContext context, Size size, String contents) {
|
||||
return Placeholder();
|
||||
}
|
||||
|
||||
String get url {
|
||||
return widget.src.toDataUrl();
|
||||
}
|
||||
}
|
||||
|
||||
enum WebNavigationDecision { navigate, prevent }
|
||||
|
||||
class WebNavigationRequest {
|
||||
WebNavigationRequest(this.url);
|
||||
|
||||
final String url;
|
||||
}
|
||||
|
||||
typedef FutureOr<WebNavigationDecision> WebNavigationDelegate(
|
||||
WebNavigationRequest webNavigationRequest);
|
||||
|
||||
class CrossWindowEvent {
|
||||
final String name;
|
||||
final Function(dynamic) eventAction;
|
||||
|
||||
CrossWindowEvent({
|
||||
required this.name,
|
||||
required this.eventAction,
|
||||
});
|
||||
}
|
||||
|
||||
typedef OnLoaded = void Function(EasyWebViewControllerWrapperBase controller);
|
||||
|
||||
abstract class EasyWebViewControllerWrapperBase {
|
||||
/// WebViewController on mobile, IFrameElement on web
|
||||
Object get nativeWrapper;
|
||||
|
||||
Future<void> evaluateJSMobile(String js);
|
||||
Future<String> evaluateJSWithResMobile(String js);
|
||||
|
||||
void postMessageWeb(dynamic message, String targetOrigin);
|
||||
}
|
||||
|
||||
class WebViewOptions {
|
||||
final WebNavigationDelegate? navigationDelegate;
|
||||
final List<CrossWindowEvent> crossWindowEvents;
|
||||
final BrowserWebViewOptions browser;
|
||||
final NativeWebViewOptions native;
|
||||
final WindowsWebViewOptions windows;
|
||||
final MarkdownOptions markdown;
|
||||
final WidgetsWebViewOptions widgets;
|
||||
|
||||
const WebViewOptions({
|
||||
this.navigationDelegate,
|
||||
this.crossWindowEvents = const [],
|
||||
this.browser = const BrowserWebViewOptions(),
|
||||
this.native = const NativeWebViewOptions(),
|
||||
this.windows = const WindowsWebViewOptions(),
|
||||
this.markdown = const MarkdownOptions(),
|
||||
this.widgets = const WidgetsWebViewOptions(),
|
||||
});
|
||||
}
|
||||
|
||||
class MarkdownOptions {
|
||||
const MarkdownOptions({
|
||||
this.selectable = false,
|
||||
this.headers = const {},
|
||||
this.convertToMarkdown,
|
||||
this.convertToHtml,
|
||||
this.onTapLink,
|
||||
});
|
||||
|
||||
final bool selectable;
|
||||
final String Function(String)? convertToMarkdown;
|
||||
final String Function(String)? convertToHtml;
|
||||
final void Function(String?)? onTapLink;
|
||||
final Map<String, String> headers;
|
||||
}
|
||||
|
||||
class WidgetsWebViewOptions {
|
||||
const WidgetsWebViewOptions({
|
||||
this.buildAsync,
|
||||
this.factoryBuilder,
|
||||
this.baseUrl,
|
||||
this.customStylesBuilder,
|
||||
this.customWidgetBuilder,
|
||||
this.onErrorBuilder,
|
||||
this.onLoadingBuilder,
|
||||
this.onTapImage,
|
||||
this.onTapUrl,
|
||||
this.rebuildTriggers,
|
||||
this.textStyle,
|
||||
this.isSelectable = false,
|
||||
this.enableCaching = true,
|
||||
this.onSelectionChanged,
|
||||
this.renderMode = RenderMode.column,
|
||||
});
|
||||
|
||||
final bool? buildAsync;
|
||||
final bool enableCaching;
|
||||
final WidgetFactory Function()? factoryBuilder;
|
||||
final bool isSelectable;
|
||||
final Uri? baseUrl;
|
||||
final CustomStylesBuilder? customStylesBuilder;
|
||||
final CustomWidgetBuilder? customWidgetBuilder;
|
||||
final OnErrorBuilder? onErrorBuilder;
|
||||
final OnLoadingBuilder? onLoadingBuilder;
|
||||
final ValueChanged<SelectedContent?>? onSelectionChanged;
|
||||
final void Function(ImageMetadata)? onTapImage;
|
||||
final FutureOr<bool> Function(String)? onTapUrl;
|
||||
final List? rebuildTriggers;
|
||||
final RenderMode renderMode;
|
||||
final TextStyle? textStyle;
|
||||
}
|
||||
|
||||
class NativeWebViewOptions {
|
||||
const NativeWebViewOptions({
|
||||
this.convertToWidgets = false,
|
||||
this.isMarkdown = false,
|
||||
this.isHtml = false,
|
||||
this.headers,
|
||||
this.widgetsTextSelectable = false,
|
||||
});
|
||||
|
||||
final bool convertToWidgets;
|
||||
final bool isMarkdown;
|
||||
final bool isHtml;
|
||||
final Map<String, String>? headers;
|
||||
final bool widgetsTextSelectable;
|
||||
}
|
||||
|
||||
class BrowserWebViewOptions {
|
||||
final bool allowFullScreen;
|
||||
final String? allow;
|
||||
|
||||
const BrowserWebViewOptions({
|
||||
this.allowFullScreen = false,
|
||||
this.allow,
|
||||
});
|
||||
}
|
||||
|
||||
class WindowsWebViewOptions {
|
||||
const WindowsWebViewOptions({
|
||||
this.popupWindowPolicy = WebviewPopupWindowPolicy.deny,
|
||||
this.backgroundColor = Colors.transparent,
|
||||
this.onUrlChanged,
|
||||
this.onError,
|
||||
this.userDataPath,
|
||||
this.browserExePath,
|
||||
this.additionalArguments,
|
||||
});
|
||||
|
||||
final WebviewPopupWindowPolicy popupWindowPolicy;
|
||||
final Color backgroundColor;
|
||||
final void Function(String)? onUrlChanged;
|
||||
final void Function(PlatformException)? onError;
|
||||
final String? userDataPath;
|
||||
final String? browserExePath;
|
||||
final String? additionalArguments;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
|
||||
import '../extensions.dart';
|
||||
import '../widgets/remote_content.dart';
|
||||
import 'base.dart';
|
||||
|
||||
class MarkdownWebView extends WebView {
|
||||
MarkdownWebView({
|
||||
required Key? key,
|
||||
required String src,
|
||||
required double? width,
|
||||
required double? height,
|
||||
required OnLoaded? onLoaded,
|
||||
required this.options,
|
||||
}) : super(
|
||||
key: key,
|
||||
src: src,
|
||||
width: width,
|
||||
height: height,
|
||||
onLoaded: onLoaded,
|
||||
);
|
||||
|
||||
final WebViewOptions options;
|
||||
|
||||
@override
|
||||
State<WebView> createState() => MarkdownState();
|
||||
}
|
||||
|
||||
class MarkdownState extends WebViewState<MarkdownWebView> {
|
||||
@override
|
||||
Widget builder(BuildContext context, Size size, String contents) {
|
||||
final options = widget.options;
|
||||
if (widget.src.isValidUrl) {
|
||||
// TODO: Check for markdown files from assets
|
||||
// TODO: Check for markdown files form file system
|
||||
return RemoteContent(
|
||||
src: widget.src,
|
||||
headers: options.markdown.headers,
|
||||
builder: (context, contents) {
|
||||
String content = contents;
|
||||
if (widget.src.isValidHtml) {
|
||||
if (options.markdown.convertToMarkdown != null) {
|
||||
content = options.markdown.convertToMarkdown!(content);
|
||||
} else {
|
||||
content = content.toMarkdown();
|
||||
}
|
||||
}
|
||||
return renderMarkdown(context, content);
|
||||
},
|
||||
);
|
||||
}
|
||||
return renderMarkdown(context, widget.src);
|
||||
}
|
||||
|
||||
Widget renderMarkdown(BuildContext context, String data) {
|
||||
final options = widget.options.markdown;
|
||||
return Markdown(
|
||||
data: data,
|
||||
onTapLink: (_, url, __) => options.onTapLink?.call(url),
|
||||
selectable: options.selectable,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart' as wv;
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
class NativeWebView extends WebView {
|
||||
const NativeWebView({
|
||||
required Key? key,
|
||||
required String src,
|
||||
required double? width,
|
||||
required double? height,
|
||||
required OnLoaded? onLoaded,
|
||||
required this.options,
|
||||
}) : super(
|
||||
key: key,
|
||||
src: src,
|
||||
width: width,
|
||||
height: height,
|
||||
onLoaded: onLoaded,
|
||||
);
|
||||
|
||||
final WebViewOptions options;
|
||||
|
||||
@override
|
||||
State<WebView> createState() => NativeWebViewState();
|
||||
}
|
||||
|
||||
class EasyWebViewControllerWrapper extends EasyWebViewControllerWrapperBase {
|
||||
final wv.WebViewController _controller;
|
||||
|
||||
EasyWebViewControllerWrapper._(this._controller);
|
||||
|
||||
@override
|
||||
Future<void> evaluateJSMobile(String js) async {
|
||||
return await _controller.runJavaScript(js);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> evaluateJSWithResMobile(String js) async {
|
||||
final result = _controller.runJavaScriptReturningResult(js);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
Object get nativeWrapper => _controller;
|
||||
|
||||
@override
|
||||
void postMessageWeb(dynamic message, String targetOrigin) =>
|
||||
throw UnsupportedError("the platform doesn't support this operation");
|
||||
}
|
||||
|
||||
class NativeWebViewState extends WebViewState<NativeWebView> {
|
||||
wv.WebViewController controller = wv.WebViewController();
|
||||
bool _initialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
reload();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant NativeWebView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.src != widget.src) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
if (!_initialized) {
|
||||
_initialized = true;
|
||||
await controller.setJavaScriptMode(wv.JavaScriptMode.unrestricted);
|
||||
await controller.setNavigationDelegate(wv.NavigationDelegate(
|
||||
onNavigationRequest: (navigationRequest) async {
|
||||
if (widget.options.navigationDelegate == null) {
|
||||
return wv.NavigationDecision.navigate;
|
||||
}
|
||||
final _navDecision = await widget.options
|
||||
.navigationDelegate!(WebNavigationRequest(navigationRequest.url));
|
||||
return _navDecision == WebNavigationDecision.prevent
|
||||
? wv.NavigationDecision.prevent
|
||||
: wv.NavigationDecision.navigate;
|
||||
},
|
||||
onPageFinished: (value) {
|
||||
if (widget.onLoaded != null) {
|
||||
widget.onLoaded!(EasyWebViewControllerWrapper._(controller));
|
||||
}
|
||||
},
|
||||
));
|
||||
if (widget.options.crossWindowEvents.isNotEmpty) {
|
||||
for (final channel in widget.options.crossWindowEvents) {
|
||||
await controller.addJavaScriptChannel(
|
||||
channel.name,
|
||||
onMessageReceived: (javascriptMessage) {
|
||||
channel.eventAction(javascriptMessage.message);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await controller.loadRequest(Uri.parse(url));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget builder(BuildContext context, Size size, String contents) {
|
||||
return wv.WebViewWidget(
|
||||
key: widget.key,
|
||||
controller: controller,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:async';
|
||||
import 'dart:html' as html;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
class BrowserWebView extends WebView {
|
||||
const BrowserWebView({
|
||||
required Key? key,
|
||||
required String src,
|
||||
required double? width,
|
||||
required double? height,
|
||||
required OnLoaded? onLoaded,
|
||||
required this.options,
|
||||
}) : super(
|
||||
key: key,
|
||||
src: src,
|
||||
width: width,
|
||||
height: height,
|
||||
onLoaded: onLoaded,
|
||||
);
|
||||
|
||||
final WebViewOptions options;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => BrowserWebViewState();
|
||||
}
|
||||
|
||||
class BrowserWebViewState extends WebViewState<BrowserWebView> {
|
||||
static final _iframeElementMap = Map<Key, html.IFrameElement>();
|
||||
bool _loaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// ignore: invalid_null_aware_operator
|
||||
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
|
||||
final _iframe = _iframeElementMap[widget.key];
|
||||
_iframe?.onLoad.listen((event) {
|
||||
widget.onLoaded?.call(EasyWebViewControllerWrapper._(_iframe));
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void setup(String? src, double width, double height) {
|
||||
final key = widget.key ?? ValueKey('');
|
||||
// ignore: undefined_prefixed_name
|
||||
ui.platformViewRegistry.registerViewFactory('iframe-$url', (int viewId) {
|
||||
if (_iframeElementMap[key] == null) {
|
||||
_iframeElementMap[key] = html.IFrameElement();
|
||||
}
|
||||
final element = _iframeElementMap[key];
|
||||
final options = widget.options.browser;
|
||||
|
||||
element!
|
||||
..style.border = '0'
|
||||
..allowFullscreen = options.allowFullScreen
|
||||
..allow = options.allow
|
||||
..height = height.toInt().toString()
|
||||
..width = width.toInt().toString();
|
||||
|
||||
html.window.addEventListener('onbeforeunload', (event) async {
|
||||
final beforeUnloadEvent = (event as html.BeforeUnloadEvent);
|
||||
if (widget.options.navigationDelegate == null) return;
|
||||
final webNavigationDecision = await widget.options.navigationDelegate!(
|
||||
WebNavigationRequest(html.window.location.href));
|
||||
if (webNavigationDecision == WebNavigationDecision.prevent) {
|
||||
// Cancel the event
|
||||
beforeUnloadEvent.preventDefault();
|
||||
// Chrome requires returnValue to be set
|
||||
beforeUnloadEvent.returnValue = '';
|
||||
} else {
|
||||
// Guarantee the browser unload by removing the returnValue property of the event
|
||||
beforeUnloadEvent.returnValue = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (widget.options.crossWindowEvents.isNotEmpty) {
|
||||
html.window.addEventListener('message', (event) {
|
||||
final eventData = (event as html.MessageEvent).data;
|
||||
widget.options.crossWindowEvents.forEach((crossWindowEvent) {
|
||||
final crossWindowEventListener = crossWindowEvent.eventAction;
|
||||
crossWindowEventListener(eventData);
|
||||
});
|
||||
});
|
||||
}
|
||||
element..src = url;
|
||||
return element;
|
||||
});
|
||||
scheduleMicrotask(() {
|
||||
setState(() {
|
||||
_loaded = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget builder(BuildContext context, ui.Size size, String contents) {
|
||||
if (!_loaded) {
|
||||
setup(widget.src, size.width, size.height);
|
||||
}
|
||||
return AbsorbPointer(
|
||||
child: RepaintBoundary(
|
||||
child: HtmlElementView(
|
||||
key: widget.key,
|
||||
viewType: 'iframe-$url',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EasyWebViewControllerWrapper extends EasyWebViewControllerWrapperBase {
|
||||
final html.IFrameElement _iframe;
|
||||
|
||||
EasyWebViewControllerWrapper._(this._iframe);
|
||||
|
||||
@override
|
||||
Future<void> evaluateJSMobile(String js) {
|
||||
throw UnsupportedError("the platform doesn't support this operation");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> evaluateJSWithResMobile(String js) {
|
||||
throw UnsupportedError("the platform doesn't support this operation");
|
||||
}
|
||||
|
||||
@override
|
||||
Object get nativeWrapper => _iframe;
|
||||
|
||||
@override
|
||||
void postMessageWeb(dynamic message, String targetOrigin) =>
|
||||
_iframe.contentWindow?.postMessage(message, targetOrigin);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import '../extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart' as wv;
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
class WidgetsWebView extends WebView {
|
||||
WidgetsWebView({
|
||||
required Key? key,
|
||||
required String src,
|
||||
required double? width,
|
||||
required double? height,
|
||||
required OnLoaded? onLoaded,
|
||||
required this.options,
|
||||
}) : super(
|
||||
key: key,
|
||||
src: src,
|
||||
width: width,
|
||||
height: height,
|
||||
onLoaded: onLoaded,
|
||||
);
|
||||
|
||||
final WebViewOptions options;
|
||||
|
||||
@override
|
||||
State<WebView> createState() => WidgetsWebViewState();
|
||||
}
|
||||
|
||||
class WidgetsWebViewState extends WebViewState<WidgetsWebView> {
|
||||
@override
|
||||
Widget builder(BuildContext context, Size size, String contents) {
|
||||
final options = widget.options.widgets;
|
||||
final Widget child = wv.HtmlWidget(
|
||||
contents.toHtml(),
|
||||
buildAsync: options.buildAsync,
|
||||
enableCaching: options.enableCaching,
|
||||
factoryBuilder: options.factoryBuilder,
|
||||
key: widget.key,
|
||||
baseUrl: options.baseUrl,
|
||||
customStylesBuilder: options.customStylesBuilder,
|
||||
customWidgetBuilder: options.customWidgetBuilder,
|
||||
onErrorBuilder: options.onErrorBuilder,
|
||||
onLoadingBuilder: options.onLoadingBuilder,
|
||||
onTapImage: options.onTapImage,
|
||||
onTapUrl: options.onTapUrl,
|
||||
rebuildTriggers: options.rebuildTriggers,
|
||||
renderMode: options.renderMode,
|
||||
textStyle: options.textStyle,
|
||||
);
|
||||
if (options.isSelectable) {
|
||||
return SelectionArea(
|
||||
onSelectionChanged: options.onSelectionChanged,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:webview_windows/webview_windows.dart' as wv;
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
class WindowsWebView extends WebView {
|
||||
WindowsWebView({
|
||||
required Key? key,
|
||||
required String src,
|
||||
required double? width,
|
||||
required double? height,
|
||||
required OnLoaded? onLoaded,
|
||||
required this.options,
|
||||
}) : super(
|
||||
key: key,
|
||||
src: src,
|
||||
width: width,
|
||||
height: height,
|
||||
onLoaded: onLoaded,
|
||||
);
|
||||
|
||||
final WebViewOptions options;
|
||||
|
||||
@override
|
||||
State<WebView> createState() => WindowsWebViewState();
|
||||
}
|
||||
|
||||
class WindowsWebViewState extends WebViewState<WindowsWebView> {
|
||||
final controller = wv.WebviewController();
|
||||
bool isSuspended = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initPlatformState();
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
await controller.loadUrl(url);
|
||||
}
|
||||
|
||||
Future<void> initPlatformState() async {
|
||||
final options = widget.options.windows;
|
||||
try {
|
||||
if (options.userDataPath != null ||
|
||||
options.browserExePath != null ||
|
||||
options.additionalArguments != null) {
|
||||
await wv.WebviewController.initializeEnvironment(
|
||||
userDataPath: options.userDataPath,
|
||||
browserExePath: options.browserExePath,
|
||||
additionalArguments: options.additionalArguments,
|
||||
);
|
||||
}
|
||||
|
||||
await controller.initialize();
|
||||
controller.url.listen((url) {
|
||||
if (options.onUrlChanged != null) {
|
||||
options.onUrlChanged!(url);
|
||||
}
|
||||
});
|
||||
|
||||
await controller.setBackgroundColor(options.backgroundColor);
|
||||
await controller.setPopupWindowPolicy(options.popupWindowPolicy);
|
||||
await reload();
|
||||
|
||||
if (mounted) setState(() {});
|
||||
} on PlatformException catch (e) {
|
||||
if (options.onError != null) {
|
||||
options.onError!(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant WindowsWebView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.src != widget.src) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget builder(BuildContext context, Size size, String contents) {
|
||||
return wv.Webview(
|
||||
controller,
|
||||
permissionRequested: (url, kind, isUserInitiated) => onPermissionRequest(
|
||||
context,
|
||||
url,
|
||||
kind,
|
||||
isUserInitiated,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<wv.WebviewPermissionDecision> onPermissionRequest(
|
||||
BuildContext context,
|
||||
String url,
|
||||
wv.WebviewPermissionKind kind,
|
||||
bool isUserInitiated,
|
||||
) async {
|
||||
final decision = await showDialog<wv.WebviewPermissionDecision>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Permission requested'),
|
||||
content: Text('Permission needed: \'$kind\''),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, wv.WebviewPermissionDecision.deny);
|
||||
},
|
||||
child: const Text('Deny'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, wv.WebviewPermissionDecision.allow);
|
||||
},
|
||||
child: const Text('Allow'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return decision ?? wv.WebviewPermissionDecision.none;
|
||||
}
|
||||
|
||||
Future<void> suspend() async {
|
||||
if (isSuspended) return;
|
||||
await controller.suspend();
|
||||
isSuspended = true;
|
||||
}
|
||||
|
||||
Future<void> resume() async {
|
||||
if (!isSuspended) return;
|
||||
await controller.resume();
|
||||
isSuspended = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'platforms/base.dart';
|
||||
import 'platforms/web.dart';
|
||||
import 'base.dart';
|
||||
|
||||
class EasyWebView extends EasyWebViewBase {
|
||||
const EasyWebView({
|
||||
Key? key,
|
||||
required String src,
|
||||
double? height,
|
||||
double? width,
|
||||
OnLoaded? onLoaded,
|
||||
bool isMarkdown = false,
|
||||
bool convertToMarkdown = false,
|
||||
bool convertToWidgets = false,
|
||||
WidgetBuilder? fallbackBuilder,
|
||||
WebViewOptions options = const WebViewOptions(),
|
||||
}) : super(
|
||||
key: key,
|
||||
src: src,
|
||||
height: height,
|
||||
width: width,
|
||||
onLoaded: onLoaded,
|
||||
isMarkdown: isMarkdown,
|
||||
convertToMarkdown: convertToMarkdown,
|
||||
convertToWidgets: convertToWidgets,
|
||||
fallbackBuilder: fallbackBuilder,
|
||||
options: options,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!canBuild()) {
|
||||
return BrowserWebView(
|
||||
key: key,
|
||||
src: src,
|
||||
width: width,
|
||||
height: height,
|
||||
onLoaded: onLoaded,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
return super.build(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OptionalSizedChild extends StatelessWidget {
|
||||
final double? width, height;
|
||||
final Widget Function(BuildContext, Size) builder;
|
||||
|
||||
const OptionalSizedChild({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, dimens) {
|
||||
final w = width ?? dimens.maxWidth;
|
||||
final h = height ?? dimens.maxHeight;
|
||||
return SizedBox(
|
||||
width: w,
|
||||
height: h,
|
||||
child: builder(context, Size(w, h)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class RemoteContent extends StatelessWidget {
|
||||
const RemoteContent({
|
||||
required this.src,
|
||||
required this.builder,
|
||||
this.headers = const {},
|
||||
});
|
||||
|
||||
final String src;
|
||||
final Map<String, String> headers;
|
||||
final Widget Function(BuildContext, String) builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<http.Response>(
|
||||
future: http.get(Uri.parse(src), headers: headers),
|
||||
builder: (context, response) {
|
||||
if (!response.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (response.data?.statusCode == 200 && response.data?.body != null) {
|
||||
String content = response.data!.body;
|
||||
return builder(context, content);
|
||||
}
|
||||
return Center(child: Icon(Icons.error));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user