TF-105 [BUG] Fix long line should be wrapped
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
|
||||
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||
import 'package:html/dom.dart';
|
||||
|
||||
class SetStyleBlockQuoteTransformer extends DomTransformer {
|
||||
|
||||
const SetStyleBlockQuoteTransformer();
|
||||
|
||||
@override
|
||||
void process(Document document, String message, TransformConfiguration configuration) {
|
||||
final quoteElements = document.getElementsByTagName('blockquote');
|
||||
for (final quote in quoteElements) {
|
||||
final style = quote.attributes['style'];
|
||||
if (style == null) {
|
||||
quote.attributes['style'] = '''
|
||||
margin: 0px 8px 0px 8px;
|
||||
padding: 8px 16px 8px 16px;
|
||||
border-left:5px solid #eee;
|
||||
''';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,23 +12,12 @@ class ImageTransformer extends DomTransformer {
|
||||
final imageElements = document.getElementsByTagName('img');
|
||||
for (final imageElement in imageElements) {
|
||||
final src = imageElement.attributes['src'];
|
||||
if (src != null) {
|
||||
if (src.startsWith('http')) {
|
||||
if (configuration.blockExternalImages) {
|
||||
imageElement.attributes.remove('src');
|
||||
} else if (src.startsWith('http:')) {
|
||||
// always at least enforce HTTPS images:
|
||||
final url = src.substring('http:'.length);
|
||||
imageElement.attributes['src'] = 'https:$url';
|
||||
}
|
||||
}
|
||||
}
|
||||
final style = imageElement.attributes['style'];
|
||||
if (style == null) {
|
||||
imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;';
|
||||
} else {
|
||||
imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;' + style;
|
||||
if (src != null && src.startsWith('http:')) {
|
||||
// always at least enforce HTTPS images:
|
||||
final url = src.substring('http:'.length);
|
||||
imageElement.attributes['src'] = 'https:$url';
|
||||
}
|
||||
imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
|
||||
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||
import 'package:html/dom.dart';
|
||||
|
||||
class SetStyleTableTransformer extends DomTransformer {
|
||||
|
||||
const SetStyleTableTransformer();
|
||||
|
||||
@override
|
||||
void process(Document document, String message, TransformConfiguration configuration) {
|
||||
final tableElements = document.getElementsByTagName('table');
|
||||
for (final table in tableElements) {
|
||||
final style = table.attributes['style'];
|
||||
if (style == null) {
|
||||
table.attributes['style'] = 'width: 100%;max-width: 100%;border:1px solid #f0f0f0;border-collapse: collapse;border-spacing: 2px;';
|
||||
}
|
||||
}
|
||||
|
||||
final tdElements = document.getElementsByTagName('td');
|
||||
for (final tdTag in tdElements) {
|
||||
final style = tdTag.attributes['style'];
|
||||
if (style == null) {
|
||||
tdTag.attributes['style'] = 'padding: 13px;margin: 0px;border:1px solid #f0f0f0;';
|
||||
}
|
||||
}
|
||||
|
||||
final thElements = document.getElementsByTagName('th');
|
||||
for (final thTag in thElements) {
|
||||
final style = thTag.attributes['style'];
|
||||
if (style == null) {
|
||||
thTag.attributes['style'] = 'padding: 13px;margin: 0px;border:1px solid #f0f0f0;';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,39 +4,22 @@ import 'package:html/dom.dart';
|
||||
|
||||
class HtmlTransform {
|
||||
|
||||
final String _message;
|
||||
final String _contentHtml;
|
||||
|
||||
HtmlTransform(this._message);
|
||||
HtmlTransform(this._contentHtml);
|
||||
|
||||
/// Transforms this message to HTML code.
|
||||
///
|
||||
/// Set [blockExternalImages] to `true` in case external images should be blocked.
|
||||
/// Optionally specify the [transformConfiguration] to control all aspects of the transformation - in that case other parameters are ignored.
|
||||
String transformToHtml({
|
||||
bool? blockExternalImages,
|
||||
TransformConfiguration? transformConfiguration,
|
||||
}) {
|
||||
final document = transformToDocument(
|
||||
blockExternalImages: blockExternalImages,
|
||||
transformConfiguration: transformConfiguration,
|
||||
);
|
||||
/// Optionally specify the [transformConfiguration] to control all aspects of the transformation
|
||||
/// - in that case other parameters are ignored.
|
||||
String transformToHtml({TransformConfiguration? transformConfiguration}) {
|
||||
final document = transformToDocument(transformConfiguration: transformConfiguration);
|
||||
return document.outerHtml;
|
||||
}
|
||||
|
||||
/// Transforms this message to Document.
|
||||
///
|
||||
/// Set [blockExternalImages] to `true` in case external images should be blocked.
|
||||
/// Optionally specify the [transformConfiguration] to control all aspects of the transformation - in that case other parameters are ignored.
|
||||
Document transformToDocument({
|
||||
bool? blockExternalImages,
|
||||
int? maxImageWidth,
|
||||
TransformConfiguration? transformConfiguration,
|
||||
}) {
|
||||
transformConfiguration ??= TransformConfiguration.create(
|
||||
blockExternalImages: blockExternalImages,
|
||||
maxImageWidth: maxImageWidth,
|
||||
);
|
||||
Document transformToDocument({TransformConfiguration? transformConfiguration}) {
|
||||
transformConfiguration ??= TransformConfiguration.create();
|
||||
final transformer = MessageContentTransformer(transformConfiguration);
|
||||
return transformer.toDocument(_message);
|
||||
return transformer.toDocument(_contentHtml);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
|
||||
import 'package:core/presentation/utils/html_transformer/dom/blockquote_transformers.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/dom/link_transformers.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/dom/meta_transformers.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/dom/table_transformers.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/text/convert_tags_text_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/text/linebreak_text_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/text/links_text_transformer.dart';
|
||||
@@ -14,12 +12,6 @@ import 'package:core/presentation/utils/html_transformer/base/text_transformer.d
|
||||
/// Contains the configuration for all transformations.
|
||||
class TransformConfiguration {
|
||||
|
||||
/// Should external images be blocked?
|
||||
final bool blockExternalImages;
|
||||
|
||||
/// The maximum width for embedded images. It make sense to limit this to reduce the generated HTML size.
|
||||
final int? maxImageWidth;
|
||||
|
||||
/// The list of DOM transformers being used
|
||||
final List<DomTransformer> domTransformers;
|
||||
|
||||
@@ -30,16 +22,12 @@ class TransformConfiguration {
|
||||
///
|
||||
/// Compare [create] to have an easier to use building function
|
||||
const TransformConfiguration(
|
||||
this.blockExternalImages,
|
||||
this.maxImageWidth,
|
||||
this.domTransformers,
|
||||
this.textTransformers
|
||||
);
|
||||
|
||||
/// Provides easy access to a standard configuration that does not block external images.
|
||||
static const TransformConfiguration standardConfiguration = TransformConfiguration(
|
||||
false,
|
||||
standardMaxImageWidth,
|
||||
standardDomTransformers,
|
||||
standardTextTransformers
|
||||
);
|
||||
@@ -61,8 +49,6 @@ class TransformConfiguration {
|
||||
: standardTextTransformers;
|
||||
maxImageWidth ??= standardMaxImageWidth;
|
||||
return TransformConfiguration(
|
||||
blockExternalImages ?? false,
|
||||
maxImageWidth,
|
||||
domTransformers,
|
||||
textTransformers
|
||||
);
|
||||
@@ -75,8 +61,6 @@ class TransformConfiguration {
|
||||
RemoveScriptTransformer(),
|
||||
ImageTransformer(),
|
||||
EnsureRelationNoReferrerTransformer(),
|
||||
SetStyleBlockQuoteTransformer(),
|
||||
SetStyleTableTransformer(),
|
||||
];
|
||||
|
||||
static const List<TextTransformer> standardTextTransformers = [
|
||||
|
||||
@@ -1,55 +1,31 @@
|
||||
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';
|
||||
import 'package:url_launcher/url_launcher.dart' as launcher;
|
||||
|
||||
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;
|
||||
final String contentHtml;
|
||||
final int minHeight;
|
||||
|
||||
/// Register this callback if you want a reference to the [InAppWebViewController].
|
||||
final void Function(InAppWebViewController controller)? onWebViewCreated;
|
||||
final void Function(InAppWebViewController controller)? onCreated;
|
||||
|
||||
final void Function(InAppWebViewController controller, Uri? uri)? onWebViewLoadStart;
|
||||
final void Function(InAppWebViewController controller, Uri? uri)? onLoadStart;
|
||||
|
||||
final void Function(InAppWebViewController controller, Uri? uri)? onWebViewLoadStop;
|
||||
final void Function(InAppWebViewController controller, Uri? uri)? onLoadStop;
|
||||
|
||||
/// Handler for mailto: links
|
||||
final Future Function(Uri mailto)? mailtoDelegate;
|
||||
|
||||
const HtmlContentViewer({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.blockExternalImages = false,
|
||||
this.onError,
|
||||
this.onWebViewCreated,
|
||||
this.onWebViewLoadStart,
|
||||
this.onWebViewLoadStop,
|
||||
required this.contentHtml,
|
||||
this.minHeight = 100,
|
||||
this.onCreated,
|
||||
this.onLoadStart,
|
||||
this.onLoadStop,
|
||||
this.mailtoDelegate,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -59,123 +35,126 @@ class HtmlContentViewer extends StatefulWidget {
|
||||
|
||||
class _HtmlContentViewState extends State<HtmlContentViewer> {
|
||||
|
||||
String? _htmlData;
|
||||
bool? _wereExternalImagesBlocked;
|
||||
bool _isGenerating = false;
|
||||
double? _webViewContentHeight = 1.0;
|
||||
double? _documentHeight = 1.0;
|
||||
double? _documentWidth = 1.0;
|
||||
String? _initialPageContent;
|
||||
late InAppWebViewController _webViewController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_generateHtml(widget.blockExternalImages);
|
||||
super.initState();
|
||||
_initialPageContent = _generateHtmlDocument(widget.contentHtml);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
String _generateHtmlDocument(String content) {
|
||||
final htmlTemplate = '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<style>
|
||||
#editor {
|
||||
outline: 0px solid transparent;
|
||||
min-height: ${widget.minHeight}px;
|
||||
min-width: 300px;
|
||||
color: #182952;
|
||||
font-family: verdana;
|
||||
}
|
||||
blockquote {
|
||||
font: normal helvetica, verdana;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 20px;
|
||||
padding-left: 15px;
|
||||
border-left: 5px solid #eee;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
td {
|
||||
padding: 13px;
|
||||
margin: 0px;
|
||||
}
|
||||
th {
|
||||
padding: 13px;
|
||||
margin: 0px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var documentHeight;
|
||||
|
||||
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);
|
||||
}
|
||||
function onLoaded() {
|
||||
documentHeight = document.body.scrollHeight;
|
||||
document.execCommand("styleWithCSS", false, true);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="onLoaded();">
|
||||
<div id="editor">$content</div>
|
||||
</body>
|
||||
</html>
|
||||
''';
|
||||
return htmlTemplate;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isGenerating) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
if (widget.blockExternalImages != _wereExternalImagesBlocked) {
|
||||
_generateHtml(widget.blockExternalImages);
|
||||
}
|
||||
|
||||
final size = MediaQuery.of(context).size;
|
||||
_documentWidth = size.width - 30;
|
||||
return SizedBox(
|
||||
height: _webViewContentHeight,
|
||||
width: size.width,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 16),
|
||||
child: _buildWebView(),
|
||||
),
|
||||
height: _documentHeight,
|
||||
width: _documentWidth,
|
||||
child: _buildWebView(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWebView() {
|
||||
if (_htmlData == null) {
|
||||
if (_initialPageContent == null || _initialPageContent?.isEmpty == true) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return InAppWebView(
|
||||
key: ValueKey(_htmlData),
|
||||
initialData: InAppWebViewInitialData(data: _htmlData!),
|
||||
key: ValueKey(_initialPageContent),
|
||||
initialData: InAppWebViewInitialData(data: _initialPageContent!),
|
||||
initialOptions: InAppWebViewGroupOptions(
|
||||
crossPlatform: InAppWebViewOptions(
|
||||
useShouldOverrideUrlLoading: true,
|
||||
verticalScrollBarEnabled: false,
|
||||
disableVerticalScroll: true,
|
||||
transparentBackground: true,
|
||||
disableVerticalScroll: false,
|
||||
disableHorizontalScroll: false,
|
||||
supportZoom: true,
|
||||
),
|
||||
android: AndroidInAppWebViewOptions(
|
||||
useWideViewPort: false,
|
||||
loadWithOverviewMode: true,
|
||||
useHybridComposition: true,
|
||||
),
|
||||
ios: IOSInAppWebViewOptions(
|
||||
allowsInlineMediaPlayback: true,
|
||||
enableViewportScale: true,
|
||||
enableViewportScale: false,
|
||||
allowsLinkPreview: false
|
||||
)
|
||||
),
|
||||
),
|
||||
onLoadStart: (controller, uri) {
|
||||
if (widget.onWebViewLoadStart != null) {
|
||||
widget.onWebViewLoadStart!(controller, uri);
|
||||
if (widget.onLoadStart != null) {
|
||||
widget.onLoadStart!(controller, uri);
|
||||
}
|
||||
},
|
||||
onWebViewCreated: widget.onWebViewCreated,
|
||||
onWebViewCreated: _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;
|
||||
final containerWidth = size.width - 60.0;
|
||||
if (scrollWidth > containerWidth) {
|
||||
var scale = (containerWidth / scrollWidth);
|
||||
if (scale < 0.2) {
|
||||
scale = 0.2;
|
||||
}
|
||||
await controller.zoomBy(zoomFactor: scale, iosAnimated: true);
|
||||
scrollHeight = (scrollHeight * scale).ceil();
|
||||
}
|
||||
final scrollHeight = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight');
|
||||
if ((scrollHeight != null) && mounted && (scrollHeight + 30.0 > widget.minHeight)) {
|
||||
setState(() {
|
||||
_webViewContentHeight = double.tryParse('${scrollHeight + 24}');
|
||||
_documentHeight = (scrollHeight + 30.0);
|
||||
});
|
||||
}
|
||||
|
||||
if (widget.onWebViewLoadStop != null) {
|
||||
widget.onWebViewLoadStop!(controller, uri);
|
||||
if (widget.onLoadStop != null) {
|
||||
widget.onLoadStop!(controller, uri);
|
||||
}
|
||||
},
|
||||
onScrollChanged: (controller, x, y) {
|
||||
if (y != 0) {
|
||||
controller.scrollTo(x: 0, y: 0);
|
||||
}
|
||||
},
|
||||
shouldOverrideUrlLoading: _shouldOverrideUrlLoading,
|
||||
@@ -185,6 +164,13 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
|
||||
);
|
||||
}
|
||||
|
||||
void _onWebViewCreated(InAppWebViewController controller) {
|
||||
_webViewController = controller;
|
||||
if (widget.onCreated != null) {
|
||||
widget.onCreated!(_webViewController);
|
||||
}
|
||||
}
|
||||
|
||||
Future<NavigationActionPolicy> _shouldOverrideUrlLoading(
|
||||
InAppWebViewController controller,
|
||||
NavigationAction request
|
||||
|
||||
Reference in New Issue
Block a user