diff --git a/core/lib/core.dart b/core/lib/core.dart index b4d3b0101..7945aed8e 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -38,6 +38,7 @@ export 'presentation/views/context_menu/simple_context_menu_action_builder.dart' export 'presentation/views/dialog/loading_dialog_builder.dart'; export 'presentation/views/dialog/downloading_file_dialog_builder.dart'; export 'presentation/views/background/background_widget_builder.dart'; +export 'presentation/views/html_viewer/html_content_viewer_widget.dart'; // Resources export 'presentation/resources/assets_paths.dart'; 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 new file mode 100644 index 000000000..1a5055881 --- /dev/null +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart @@ -0,0 +1,171 @@ +import 'package:core/core.dart'; +import 'package:core/presentation/utils/html_transformer/html_transform.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +class _HtmlGenerationArguments { + final String message; + final bool blockExternalImages; + + const _HtmlGenerationArguments( + this.message, + this.blockExternalImages, + ); +} + +class _HtmlGenerationResult { + final String? html; + final String? errorDetails; + + const _HtmlGenerationResult.success(this.html) : errorDetails = null; + + const _HtmlGenerationResult.error(this.errorDetails) : this.html = null; +} + +class HtmlContentViewer extends StatefulWidget { + + final String message; + final bool blockExternalImages; + + /// Is notified about any errors that might occur + final void Function(Object? exception, StackTrace? stackTrace)? onError; + + /// Register this callback if you want a reference to the [InAppWebViewController]. + final void Function(InAppWebViewController controller)? onWebViewCreated; + + const HtmlContentViewer({ + Key? key, + required this.message, + this.blockExternalImages = false, + this.onError, + this.onWebViewCreated, + }) : super(key: key); + + @override + State createState() => _HtmlContentViewState(); +} + +class _HtmlContentViewState extends State { + + String? _htmlData; + bool? _wereExternalImagesBlocked; + bool _isGenerating = false; + double? _webViewContentHeight; + + @override + void initState() { + _generateHtml(widget.blockExternalImages); + super.initState(); + } + + void _generateHtml(bool blockExternalImages) async { + _wereExternalImagesBlocked = blockExternalImages; + _isGenerating = true; + final args = _HtmlGenerationArguments( + widget.message, + blockExternalImages, + ); + final result = await compute(_generateHtmlImpl, args); + _htmlData = result.html; + if (_htmlData == null) { + final onError = widget.onError; + if (onError != null) { + onError(result.errorDetails, null); + } + } + if (mounted) { + setState(() { + _isGenerating = false; + }); + } + } + + static _HtmlGenerationResult _generateHtmlImpl(_HtmlGenerationArguments args) { + try { + final htmlTransform = HtmlTransform(args.message); + final html = htmlTransform.transformToHtml( + blockExternalImages: args.blockExternalImages, + ); + return _HtmlGenerationResult.success(html); + } catch (e, s) { + String errorDetails = e.toString() + '\n\n' + s.toString(); + return _HtmlGenerationResult.error(errorDetails); + } + } + + @override + Widget build(BuildContext context) { + if (_isGenerating) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: SizedBox( + width: 30, + height: 30, + child: CircularProgressIndicator(color: AppColor.primaryColor)), + ), + ); + } + if (widget.blockExternalImages != _wereExternalImagesBlocked) { + _generateHtml(widget.blockExternalImages); + } + + final size = MediaQuery.of(context).size; + return SizedBox( + height: _webViewContentHeight ?? size.height, + width: size.width, + child: Padding( + padding: EdgeInsets.only(top: 16), + child: _buildWebView(), + ), + ); + } + + Widget _buildWebView() { + if (_htmlData == null) { + return Container(); + } + + return InAppWebView( + key: ValueKey(_htmlData), + initialData: InAppWebViewInitialData(data: _htmlData!), + initialOptions: InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + useShouldOverrideUrlLoading: true, + verticalScrollBarEnabled: false, + ), + android: AndroidInAppWebViewOptions( + useHybridComposition: true, + ), + ios: IOSInAppWebViewOptions( + allowsInlineMediaPlayback: true, + enableViewportScale: true, + ) + ), + onWebViewCreated: widget.onWebViewCreated, + onLoadStop: (controller, uri) async { + var scrollHeight = (await controller.evaluateJavascript(source: 'document.body.scrollHeight')); + if (scrollHeight != null) { + final scrollWidth = (await controller.evaluateJavascript(source: 'document.body.scrollWidth')); + final size = MediaQuery.of(context).size; + if (scrollWidth > size.width) { + var scale = (size.width / scrollWidth); + if (scale < 0.2) { + scale = 0.2; + } + await controller.zoomBy(zoomFactor: scale, iosAnimated: true); + scrollHeight = (scrollHeight * scale).ceil(); + } + setState(() { + _webViewContentHeight = double.tryParse('${scrollHeight + 10.0}'); + }); + } + }, + gestureRecognizers: { + Factory(() => LongPressGestureRecognizer()), + }, + ); + } +} \ No newline at end of file diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 8d845e491..32430f257 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -59,6 +59,13 @@ dependencies: # getwidget getwidget: 2.0.4 + # Used to fix bugs in the official version of flutter_inappwebview: https://githubmemory.com/repo/pichillilorenzo/flutter_inappwebview/issues/873 + # flutter_inappwebview + flutter_inappwebview: + git: + url: git://github.com/robert-virkus/flutter_inappwebview.git + ref: master + dev_dependencies: flutter_test: sdk: flutter