adding packages

This commit is contained in:
2026-01-15 14:38:46 -08:00
parent ef86e3ab6a
commit 6869cf47e5
5253 changed files with 726695 additions and 34 deletions
@@ -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();
}
}
+62
View File
@@ -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;
}
}
+46
View File
@@ -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));
},
);
}
}