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.

257 lines
9.5 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
  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. textAlign: TextAlign.justify,
  159. fontSize: FontSize(fontSizeFactor * 2.3, Unit.rem),
  160. ),
  161. 'h2': Style(
  162. color: textColor,
  163. fontWeight: FontWeight.normal,
  164. textAlign: TextAlign.justify,
  165. fontSize: FontSize(fontSizeFactor * 2.1, Unit.rem),
  166. ),
  167. 'h3': Style(
  168. color: textColor,
  169. fontWeight: FontWeight.normal,
  170. textAlign: TextAlign.justify,
  171. fontSize: FontSize(fontSizeFactor * 1.9, Unit.rem),
  172. ),
  173. 'h4': Style(
  174. color: textColor,
  175. fontWeight: FontWeight.normal,
  176. textAlign: TextAlign.justify,
  177. fontSize: FontSize(fontSizeFactor * 1.7, Unit.rem),
  178. ),
  179. 'h5': Style(
  180. color: textColor,
  181. fontWeight: FontWeight.normal,
  182. textAlign: TextAlign.justify,
  183. fontSize: FontSize(fontSizeFactor * 1.6, Unit.rem),
  184. ),
  185. 'h6': Style(
  186. color: textColor,
  187. fontWeight: FontWeight.normal,
  188. textAlign: TextAlign.justify,
  189. fontSize: FontSize(fontSizeFactor * 1.4, Unit.rem),
  190. ),
  191. 'li': Style(
  192. fontWeight: FontWeight.normal,
  193. textAlign: TextAlign.justify,
  194. fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
  195. ),
  196. 'a': Style(
  197. color: textColor,
  198. fontWeight: FontWeight.normal,
  199. textAlign: TextAlign.justify,
  200. fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
  201. ),
  202. 'ol': Style(
  203. fontWeight: FontWeight.normal,
  204. fontSize: FontSize(fontSizeFactor * 1.3, Unit.rem),
  205. textAlign: TextAlign.justify,
  206. ),
  207. 'html': Style(
  208. fontSize: FontSize(baseFontSize * fontSizeFactor),
  209. ),
  210. '*': Style.fromTextStyle(style).copyWith(
  211. color: textColor,
  212. lineHeight: LineHeight.rem(lineHeight),
  213. fontSize: FontSize(fontSizeFactor * baseFontSize),
  214. padding: const EdgeInsets.symmetric(vertical: 8),
  215. ),
  216. },
  217. tagsList: Html.tags..addAll(['flutter', 'q_header', 'q_text', 'q_answer', 't_header']),
  218. );
  219. },
  220. );
  221. return html;
  222. }
  223. void _openImage({required String imageUrl, required BuildContext context}) {
  224. Navigator.push(context, MaterialPageRoute(
  225. builder: (context) {
  226. return ShowImageWidget(imageUrl);
  227. },
  228. ));
  229. }
  230. }
  231. CustomRenderMatcher _stringMatcher(String tag) => (context) => context.tree.element?.localName == tag;
  232. class _RoundFrame extends StatelessWidget {
  233. final Widget child;
  234. final bool hasFullWidth;
  235. const _RoundFrame({super.key, required this.child, this.hasFullWidth = true});
  236. @override
  237. Widget build(BuildContext context) {
  238. return Container(
  239. width: hasFullWidth ? 1.sw : null,
  240. margin: Utils.instance.singleMargin(top: 7, bottom: 7),
  241. child: ClipRRect(
  242. borderRadius: const BorderRadius.all(Radius.circular(8)),
  243. child: child,
  244. ),
  245. );
  246. }
  247. }