Sonnat Project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

251 lines
9.2 KiB

  1. import 'package:cached_network_image/cached_network_image.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:sonnat/core/extensions/number_extension.dart';
  4. import 'package:sonnat/core/html/custom_render.dart';
  5. import 'package:sonnat/core/html/flutter_html.dart';
  6. import 'package:sonnat/core/html/src/style/fontsize.dart';
  7. import 'package:sonnat/core/html/src/style/length.dart';
  8. import 'package:sonnat/core/html/src/style/lineheight.dart';
  9. import 'package:sonnat/core/html/string_proccess.dart';
  10. import 'package:sonnat/core/html/style.dart';
  11. import 'package:sonnat/core/player_widgets/audio_player.dart';
  12. import 'package:sonnat/core/player_widgets/video_player.dart';
  13. import 'package:sonnat/core/theme/app_colors.dart';
  14. import 'package:sonnat/core/theme/app_theme.dart';
  15. import 'package:sonnat/core/theme/reader_theme.dart';
  16. import 'package:sonnat/core/utils/app_utils.dart';
  17. import 'package:sonnat/core/widgets/show_image_widget.dart';
  18. import 'package:url_launcher/url_launcher.dart';
  19. class HTMLViewer extends StatelessWidget {
  20. final String htmlContent;
  21. final double fontSizeFactor;
  22. final bool needToReplaceTags;
  23. final ReaderTheme? theme;
  24. final String? searchHighLight;
  25. final double baseFontSize = 16.0;
  26. final Color? textColor;
  27. const HTMLViewer({
  28. super.key,
  29. required this.htmlContent,
  30. this.fontSizeFactor = 1,
  31. this.needToReplaceTags = false,
  32. this.theme = ReaderTheme.light,
  33. this.searchHighLight,
  34. this.textColor,
  35. });
  36. @override
  37. Widget build(BuildContext context) {
  38. var style = AppTheme.instance.fontCreator(
  39. 17,
  40. FontWeights.regular,
  41. AppColors.settingSemiBlack,
  42. FontFamilyName.segoeui,
  43. -0.0,
  44. 1.5,
  45. );
  46. Widget html = Builder(
  47. builder: (context) {
  48. double lineHeight = Theme.of(context).textTheme.displayLarge?.height ?? 1.1;
  49. return Html(
  50. data: needToReplaceTags
  51. ? htmlContent.replaceTHeader().replaceQHeader().replaceQText().replaceQAnswer().replaceTextStyle()
  52. : htmlContent.replaceTextStyle(),
  53. onLinkTap: (url, context, attributes, element) {
  54. if (url == null) {
  55. return;
  56. }
  57. launchUrl(Uri.parse(url)).then((value) {
  58. return null;
  59. });
  60. },
  61. customRenders: {
  62. _stringMatcher('video'): CustomRender.widget(widget: (context, buildChildren) {
  63. return _RoundFrame(
  64. child: VideoPlayer(
  65. url: context.tree.element!.attributes['src'] ?? '',
  66. ),
  67. );
  68. }),
  69. _stringMatcher('img'): CustomRender.widget(widget: (renderContext, buildChildren) {
  70. return GestureDetector(
  71. onTap: () {
  72. if (renderContext.tree.element!.attributes['src'] == null) {
  73. return;
  74. }
  75. _openImage(
  76. imageUrl: renderContext.tree.element!.attributes['src'] ?? '',
  77. context: context,
  78. );
  79. },
  80. child: _RoundFrame(
  81. child: CachedNetworkImage(
  82. imageUrl: renderContext.tree.element!.attributes['src'] ?? '',
  83. ),
  84. ),
  85. );
  86. }),
  87. _stringMatcher('audio'): CustomRender.widget(widget: (context, buildChildren) {
  88. return AudioPlayer(
  89. url: context.tree.element!.nodes[1].attributes['src'] ?? '',
  90. );
  91. }),
  92. _stringMatcher('q_header'): CustomRender.widget(widget: (context, buildChildren) {
  93. if (context.tree.element?.hasChildNodes() ?? false) {
  94. if (context.tree.element?.firstChild?.text != null) {
  95. String txt = context.tree.element?.firstChild?.text ?? '';
  96. return QHeaderTextShower(
  97. title: txt,
  98. searchHighLight: searchHighLight,
  99. fontSizeFactor: fontSizeFactor,
  100. );
  101. }
  102. }
  103. return const _RoundFrame(child: SizedBox());
  104. }),
  105. _stringMatcher('q_text'): CustomRender.widget(widget: (context, buildChildren) {
  106. if (context.tree.element?.hasChildNodes() ?? false) {
  107. if (context.tree.element?.firstChild?.text != null) {
  108. String txt = context.tree.element?.firstChild?.text ?? '';
  109. return QTextShower(
  110. title: txt,
  111. searchHighLight: searchHighLight,
  112. fontSizeFactor: fontSizeFactor,
  113. theme: theme,
  114. );
  115. }
  116. }
  117. return const _RoundFrame(child: SizedBox());
  118. }),
  119. _stringMatcher('q_answer'): CustomRender.widget(widget: (context, buildChildren) {
  120. if (context.tree.element?.hasChildNodes() ?? false) {
  121. if (context.tree.element?.firstChild?.text != null) {
  122. String txt = context.tree.element?.firstChild?.text ?? '';
  123. return QAnswerShower(
  124. title: txt,
  125. searchHighLight: searchHighLight,
  126. fontSizeFactor: fontSizeFactor,
  127. theme: theme,
  128. );
  129. }
  130. }
  131. return const _RoundFrame(child: SizedBox());
  132. }),
  133. _stringMatcher('t_header'): CustomRender.widget(widget: (context, buildChildren) {
  134. if (context.tree.element?.hasChildNodes() ?? false) {
  135. if (context.tree.element?.firstChild?.text != null) {
  136. String txt = context.tree.element?.firstChild?.text ?? '';
  137. return THeaderTextShower(
  138. title: txt,
  139. searchHighLight: searchHighLight,
  140. fontSizeFactor: fontSizeFactor,
  141. theme: theme,
  142. );
  143. }
  144. }
  145. return const _RoundFrame(child: SizedBox());
  146. }),
  147. },
  148. style: {
  149. 'p': Style(
  150. color: textColor,
  151. fontWeight: FontWeight.normal,
  152. fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
  153. textAlign: TextAlign.justify,
  154. ),
  155. 'h1': Style(
  156. color: textColor,
  157. fontWeight: FontWeight.normal,
  158. fontSize: FontSize(fontSizeFactor * 2.3, Unit.rem),
  159. ),
  160. 'h2': Style(
  161. color: textColor,
  162. fontWeight: FontWeight.normal,
  163. fontSize: FontSize(fontSizeFactor * 2.1, Unit.rem),
  164. ),
  165. 'h3': Style(
  166. color: textColor,
  167. fontWeight: FontWeight.normal,
  168. fontSize: FontSize(fontSizeFactor * 1.9, Unit.rem),
  169. ),
  170. 'h4': Style(
  171. color: textColor,
  172. fontWeight: FontWeight.normal,
  173. fontSize: FontSize(fontSizeFactor * 1.7, Unit.rem),
  174. ),
  175. 'h5': Style(
  176. color: textColor,
  177. fontWeight: FontWeight.normal,
  178. fontSize: FontSize(fontSizeFactor * 1.6, Unit.rem),
  179. ),
  180. 'h6': Style(
  181. color: textColor,
  182. fontWeight: FontWeight.normal,
  183. fontSize: FontSize(fontSizeFactor * 1.4, Unit.rem),
  184. ),
  185. 'li': Style(
  186. fontWeight: FontWeight.normal,
  187. fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
  188. ),
  189. 'a': Style(
  190. color: textColor,
  191. fontWeight: FontWeight.normal,
  192. fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
  193. ),
  194. 'ol': Style(
  195. fontWeight: FontWeight.normal,
  196. fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
  197. ),
  198. 'html': Style(
  199. fontSize: FontSize(baseFontSize * fontSizeFactor),
  200. ),
  201. '*': Style.fromTextStyle(style).copyWith(
  202. color: textColor,
  203. lineHeight: LineHeight.rem(lineHeight),
  204. fontSize: FontSize(fontSizeFactor * baseFontSize),
  205. padding: const EdgeInsets.symmetric(vertical: 8),
  206. ),
  207. },
  208. tagsList: Html.tags..addAll(['flutter', 'q_header', 'q_text', 'q_answer', 't_header']),
  209. );
  210. },
  211. );
  212. return Padding(
  213. padding: Utils.instance.singleMargin(left: 15, right: 15, bottom: 60.h),
  214. child: html,
  215. );
  216. }
  217. void _openImage({required String imageUrl, required BuildContext context}) {
  218. Navigator.push(context, MaterialPageRoute(
  219. builder: (context) {
  220. return ShowImageWidget(imageUrl);
  221. },
  222. ));
  223. }
  224. }
  225. CustomRenderMatcher _stringMatcher(String tag) => (context) => context.tree.element?.localName == tag;
  226. class _RoundFrame extends StatelessWidget {
  227. final Widget child;
  228. final bool hasFullWidth;
  229. const _RoundFrame({super.key, required this.child, this.hasFullWidth = true});
  230. @override
  231. Widget build(BuildContext context) {
  232. return Container(
  233. width: hasFullWidth ? 1.sw : null,
  234. margin: Utils.instance.singleMargin(top: 7, bottom: 7),
  235. child: ClipRRect(
  236. borderRadius: const BorderRadius.all(Radius.circular(8)),
  237. child: child,
  238. ),
  239. );
  240. }
  241. }