import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:sonnat/core/extensions/number_extension.dart'; import 'package:sonnat/core/html/custom_render.dart'; import 'package:sonnat/core/html/flutter_html.dart'; import 'package:sonnat/core/html/src/style/fontsize.dart'; import 'package:sonnat/core/html/src/style/length.dart'; import 'package:sonnat/core/html/src/style/lineheight.dart'; import 'package:sonnat/core/html/string_proccess.dart'; import 'package:sonnat/core/html/style.dart'; import 'package:sonnat/core/player_widgets/audio_player.dart'; import 'package:sonnat/core/player_widgets/video_player.dart'; import 'package:sonnat/core/theme/app_colors.dart'; import 'package:sonnat/core/theme/app_theme.dart'; import 'package:sonnat/core/theme/reader_theme.dart'; import 'package:sonnat/core/utils/app_utils.dart'; import 'package:sonnat/core/widgets/show_image_widget.dart'; import 'package:url_launcher/url_launcher.dart'; class HTMLViewer extends StatelessWidget { final String htmlContent; final double fontSizeFactor; final bool needToReplaceTags; final ReaderTheme? theme; final String? searchHighLight; final double baseFontSize = 16.0; final Color? textColor; const HTMLViewer({ super.key, required this.htmlContent, this.fontSizeFactor = 1, this.needToReplaceTags = false, this.theme = ReaderTheme.light, this.searchHighLight, this.textColor, }); @override Widget build(BuildContext context) { var style = AppTheme.instance.fontCreator( 17, FontWeights.regular, AppColors.settingSemiBlack, FontFamilyName.segoeui, -0.0, 1.5, ); Widget html = Builder( builder: (context) { double lineHeight = Theme.of(context).textTheme.displayLarge?.height ?? 1.1; return Html( data: needToReplaceTags ? htmlContent.replaceTHeader().replaceQHeader().replaceQText().replaceQAnswer().replaceTextStyle() : htmlContent.replaceTextStyle(), onLinkTap: (url, context, attributes, element) { if (url == null) { return; } launchUrl(Uri.parse(url)).then((value) { return null; }); }, customRenders: { _stringMatcher('video'): CustomRender.widget(widget: (context, buildChildren) { return _RoundFrame( child: VideoPlayer( url: context.tree.element!.attributes['src'] ?? '', ), ); }), _stringMatcher('img'): CustomRender.widget(widget: (renderContext, buildChildren) { return GestureDetector( onTap: () { if (renderContext.tree.element!.attributes['src'] == null) { return; } _openImage( imageUrl: renderContext.tree.element!.attributes['src'] ?? '', context: context, ); }, child: _RoundFrame( child: CachedNetworkImage( imageUrl: renderContext.tree.element!.attributes['src'] ?? '', ), ), ); }), _stringMatcher('audio'): CustomRender.widget(widget: (context, buildChildren) { return AudioPlayer( url: context.tree.element!.nodes[1].attributes['src'] ?? '', ); }), _stringMatcher('q_header'): CustomRender.widget(widget: (context, buildChildren) { if (context.tree.element?.hasChildNodes() ?? false) { if (context.tree.element?.firstChild?.text != null) { String txt = context.tree.element?.firstChild?.text ?? ''; return QHeaderTextShower( title: txt, searchHighLight: searchHighLight, fontSizeFactor: fontSizeFactor, ); } } return const _RoundFrame(child: SizedBox()); }), _stringMatcher('q_text'): CustomRender.widget(widget: (context, buildChildren) { if (context.tree.element?.hasChildNodes() ?? false) { if (context.tree.element?.firstChild?.text != null) { String txt = context.tree.element?.firstChild?.text ?? ''; return QTextShower( title: txt, searchHighLight: searchHighLight, fontSizeFactor: fontSizeFactor, theme: theme, ); } } return const _RoundFrame(child: SizedBox()); }), _stringMatcher('q_answer'): CustomRender.widget(widget: (context, buildChildren) { if (context.tree.element?.hasChildNodes() ?? false) { if (context.tree.element?.firstChild?.text != null) { String txt = context.tree.element?.firstChild?.text ?? ''; return QAnswerShower( title: txt, searchHighLight: searchHighLight, fontSizeFactor: fontSizeFactor, theme: theme, ); } } return const _RoundFrame(child: SizedBox()); }), _stringMatcher('t_header'): CustomRender.widget(widget: (context, buildChildren) { if (context.tree.element?.hasChildNodes() ?? false) { if (context.tree.element?.firstChild?.text != null) { String txt = context.tree.element?.firstChild?.text ?? ''; return THeaderTextShower( title: txt, searchHighLight: searchHighLight, fontSizeFactor: fontSizeFactor, theme: theme, ); } } return const _RoundFrame(child: SizedBox()); }), }, style: { 'p': Style( color: textColor, fontWeight: FontWeight.normal, fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem), textAlign: TextAlign.justify, ), 'h1': Style( color: textColor, fontWeight: FontWeight.normal, textAlign: TextAlign.justify, fontSize: FontSize(fontSizeFactor * 2.3, Unit.rem), ), 'h2': Style( color: textColor, fontWeight: FontWeight.normal, textAlign: TextAlign.justify, fontSize: FontSize(fontSizeFactor * 2.1, Unit.rem), ), 'h3': Style( color: textColor, fontWeight: FontWeight.normal, textAlign: TextAlign.justify, fontSize: FontSize(fontSizeFactor * 1.9, Unit.rem), ), 'h4': Style( color: textColor, fontWeight: FontWeight.normal, textAlign: TextAlign.justify, fontSize: FontSize(fontSizeFactor * 1.7, Unit.rem), ), 'h5': Style( color: textColor, fontWeight: FontWeight.normal, textAlign: TextAlign.justify, fontSize: FontSize(fontSizeFactor * 1.6, Unit.rem), ), 'h6': Style( color: textColor, fontWeight: FontWeight.normal, textAlign: TextAlign.justify, fontSize: FontSize(fontSizeFactor * 1.4, Unit.rem), ), 'li': Style( fontWeight: FontWeight.normal, textAlign: TextAlign.justify, fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem), ), 'a': Style( color: textColor, fontWeight: FontWeight.normal, textAlign: TextAlign.justify, fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem), ), 'ol': Style( fontWeight: FontWeight.normal, fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem), textAlign: TextAlign.justify, ), 'html': Style( fontSize: FontSize(baseFontSize * fontSizeFactor), ), '*': Style.fromTextStyle(style).copyWith( color: textColor, lineHeight: LineHeight.rem(lineHeight), fontSize: FontSize(fontSizeFactor * baseFontSize), padding: const EdgeInsets.symmetric(vertical: 8), ), }, tagsList: Html.tags..addAll(['flutter', 'q_header', 'q_text', 'q_answer', 't_header']), ); }, ); return html; } void _openImage({required String imageUrl, required BuildContext context}) { Navigator.push(context, MaterialPageRoute( builder: (context) { return ShowImageWidget(imageUrl); }, )); } } CustomRenderMatcher _stringMatcher(String tag) => (context) => context.tree.element?.localName == tag; class _RoundFrame extends StatelessWidget { final Widget child; final bool hasFullWidth; const _RoundFrame({super.key, required this.child, this.hasFullWidth = true}); @override Widget build(BuildContext context) { return Container( width: hasFullWidth ? 1.sw : null, margin: Utils.instance.singleMargin(top: 7, bottom: 7), child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), child: child, ), ); } }