diff --git a/core/lib/core.dart b/core/lib/core.dart index 2e9eb88be..7b5dc9e41 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -45,6 +45,8 @@ export 'presentation/views/dialog/confirmation_dialog_builder.dart'; export 'presentation/views/dialog/edit_text_dialog_builder.dart'; export 'presentation/views/background/background_widget_builder.dart'; export 'presentation/views/html_viewer/html_content_viewer_widget.dart'; +export 'presentation/views/html_viewer/html_content_viewer_on_web_widget.dart'; +export 'presentation/views/html_viewer/html_viewer_controller_for_web.dart'; export 'presentation/views/floating_button/scrolling_floating_button_animated.dart'; export 'presentation/views/bottom_popup/cupertino_action_sheet_action_builder.dart'; export 'presentation/views/bottom_popup/cupertino_action_sheet_builder.dart'; diff --git a/core/lib/presentation/utils/shims/dart_ui.dart b/core/lib/presentation/utils/shims/dart_ui.dart new file mode 100644 index 000000000..3567e0f26 --- /dev/null +++ b/core/lib/presentation/utils/shims/dart_ui.dart @@ -0,0 +1,10 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// This file shims dart:ui in web-only scenarios, getting rid of the need to +/// suppress analyzer warnings. + +// TODO(): flutter/flutter#55000 Remove this file once web-only dart:ui APIs +// are exposed from a dedicated place. +export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart'; diff --git a/core/lib/presentation/utils/shims/dart_ui_fake.dart b/core/lib/presentation/utils/shims/dart_ui_fake.dart new file mode 100644 index 000000000..386e46e01 --- /dev/null +++ b/core/lib/presentation/utils/shims/dart_ui_fake.dart @@ -0,0 +1,21 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Fake interface for the logic that this package needs from (web-only) dart:ui. +// This is conditionally exported so the analyzer sees these methods as available. + +/// Shim for web_ui engine.PlatformViewRegistry +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +// ignore: camel_case_types +class platformViewRegistry { + /// Shim for registerViewFactory + /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 + static void registerViewFactory( + String viewTypeId, dynamic Function(int viewId) viewFactory) {} +} + +/// Signature of callbacks that have no arguments and return no data. +typedef VoidCallback = void Function(); + +dynamic get window => null; diff --git a/core/lib/presentation/utils/shims/dart_ui_real.dart b/core/lib/presentation/utils/shims/dart_ui_real.dart new file mode 100644 index 000000000..90ecd7d00 --- /dev/null +++ b/core/lib/presentation/utils/shims/dart_ui_real.dart @@ -0,0 +1,5 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'dart:ui'; diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart new file mode 100644 index 000000000..173e31ab9 --- /dev/null +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart @@ -0,0 +1,232 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/html_viewer/html_viewer_controller_for_web.dart'; +import 'package:flutter/material.dart'; +import 'dart:developer' as developer; +import 'package:universal_html/html.dart' as html; +import 'package:core/presentation/utils/shims/dart_ui.dart' as ui; + +class HtmlContentViewerOnWeb extends StatefulWidget { + + final String contentHtml; + final double widthContent; + final HtmlViewerControllerForWeb controller; + + const HtmlContentViewerOnWeb({ + Key? key, + required this.contentHtml, + required this.widthContent, + required this.controller, + }) : super(key: key); + + @override + _HtmlContentViewerOnWebState createState() => _HtmlContentViewerOnWebState(); +} + +class _HtmlContentViewerOnWebState extends State { + + /// The view ID for the IFrameElement. Must be unique. + late String createdViewId; + /// The actual height of the editor, used to automatically set the height + late double actualHeight; + + Future? webInit; + String? _htmlData; + bool _isLoading = true; + int minHeight = 100; + + @override + void initState() { + super.initState(); + actualHeight = 400; + createdViewId = _getRandString(10); + widget.controller.viewId = createdViewId; + _setUpWeb(); + } + + String _getRandString(int len) { + var random = Random.secure(); + var values = List.generate(len, (i) => random.nextInt(255)); + return base64UrlEncode(values); + } + + String _generateHtmlDocument(String content) { + final htmlScripts = ''' + + '''; + + final htmlTemplate = ''' + + + + + + + $htmlScripts + + +
$content
+ + + '''; + + return htmlTemplate; + } + + void _setUpWeb() { + _htmlData = _generateHtmlDocument(widget.contentHtml); + + final iframe = html.IFrameElement() + ..width = widget.widthContent.toString() + ..height = actualHeight.toString() + ..srcdoc = _htmlData ?? '' + ..style.border = 'none' + ..style.overflowY = 'hidden' + ..onLoad.listen((event) async { + var data = {'type': 'toIframe: getHeight'}; + data['view'] = createdViewId; + final jsonEncoder = JsonEncoder(); + var jsonStr = jsonEncoder.convert(data); + html.window.postMessage(jsonStr, '*'); + + html.window.onBeforeUnload.listen((event) { + event.preventDefault(); + }); + + html.window.onMessage.listen((event) { + var data = json.decode(event.data); + developer.log('onMessage(): data: $data', name: 'HtmlContentViewerOnWeb'); + if (data['type'] != null && + data['type'].contains('toDart: htmlHeight') && + data['view'] == createdViewId) { + final docHeight = data['height'] ?? actualHeight; + developer.log('onMessage(): docHeight: $docHeight', name: 'HtmlContentViewerOnWeb'); + if (docHeight != null && mounted) { + final scrollHeightWithBuffer = docHeight + 30.0; + if (scrollHeightWithBuffer > minHeight) { + setState(() { + actualHeight = scrollHeightWithBuffer; + _isLoading = false; + }); + } + } + if (mounted && _isLoading) { + setState(() { + _isLoading = false; + }); + } + } + if (data['type'] != null && + data['type'].contains('toDart: onChangeContent') && + data['view'] == createdViewId) { + if (Scrollable.of(context) != null) { + Scrollable.of(context)!.position.ensureVisible( + context.findRenderObject()!, + duration: const Duration(milliseconds: 100), + curve: Curves.easeIn); + } + } + }); + }); + + ui.platformViewRegistry.registerViewFactory(createdViewId, (int viewId) => iframe); + + if (mounted) { + setState(() { + webInit = Future.value(true); + }); + } + } + + @override + Widget build(BuildContext context) { + return Stack( + alignment: AlignmentDirectional.center, + children: [ + SizedBox( + height: actualHeight, + width: widget.widthContent, + child: _buildWebView(), + ), + if (_isLoading) _buildLoadingView() + ], + ); + } + + Widget _buildLoadingView() { + return Padding( + padding: EdgeInsets.all(16), + child: SizedBox( + width: 30, + height: 30, + child: CircularProgressIndicator(color: AppColor.colorTextButton))); + } + + Widget _buildWebView() { + final htmlData = _htmlData; + if (htmlData == null || htmlData.isEmpty) { + return Container(); + } + + return Directionality( + textDirection: TextDirection.ltr, + child: FutureBuilder( + future: webInit, + builder: (context, snapshot) { + if (snapshot.hasData) { + return HtmlElementView( + key: ValueKey(htmlData), + viewType: createdViewId, + ); + } else { + return Container(); + } + } + ) + ); + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart index 3d0c09c8d..6ed7ecff4 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:core/core.dart'; -import 'package:easy_web_view/easy_web_view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -12,9 +11,7 @@ import 'dart:developer' as developer; class HtmlContentViewer extends StatefulWidget { final String contentHtml; - final int minHeight; final double widthContent; - final double? heightContent; final Widget? loadingWidget; /// Register this callback if you want a reference to the [WebViewController]. @@ -32,8 +29,6 @@ class HtmlContentViewer extends StatefulWidget { Key? key, required this.contentHtml, required this.widthContent, - this.heightContent, - this.minHeight = 100, this.loadingWidget, this.onCreated, this.urlLauncherDelegate, @@ -48,9 +43,9 @@ class _HtmlContentViewState extends State { double? _webViewHeight = 1.0; double? _webViewWidth = 1.0; + int minHeight = 100; String? _htmlData; late WebViewController _webViewController; - // late EasyWebViewControllerWrapperBase _easyWebViewControllerWrapperBase; bool _isLoading = true; @override @@ -69,7 +64,7 @@ class _HtmlContentViewState extends State {