From 1aba1b7d7d1e56d3cc4b1bfb63a98180c7fa0f53 Mon Sep 17 00:00:00 2001 From: Niraj Nandish Date: Thu, 26 Jun 2025 21:45:00 +0530 Subject: [PATCH 1/3] chore: add collection package --- mobile-app/pubspec.lock | 2 +- mobile-app/pubspec.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index b22ecc374..07545d99a 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -295,7 +295,7 @@ packages: source: hosted version: "4.10.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 9944fb16d..eacc25a2c 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: audio_service: 0.18.18 auth0_flutter: 1.9.0 cached_network_image: 3.4.1 + collection: 1.19.1 device_info_plus: 11.3.0 dio: 5.8.0+1 firebase_analytics: 11.4.5 From a162dfdd970d821fbcd80c6b0e9cc59be3f0b0ee Mon Sep 17 00:00:00 2001 From: Niraj Nandish Date: Thu, 26 Jun 2025 21:49:37 +0530 Subject: [PATCH 2/3] feat: customise html styles --- .../templates/dialogue/dialogue_view.dart | 9 +- .../learn/block/templates/grid/grid_view.dart | 9 +- .../learn/block/templates/link/link_view.dart | 9 +- .../learn/block/templates/list/list_view.dart | 9 +- .../views/learn/challenge/challenge_view.dart | 9 +- .../templates/english/english_view.dart | 11 +- .../challenge/templates/quiz/quiz_view.dart | 11 +- .../templates/review/review_viewmodel.dart | 13 +- .../learn/widgets/assignment_widget.dart | 10 +- .../ui/views/learn/widgets/quiz_widget.dart | 11 +- .../views/news/html_handler/html_handler.dart | 193 +++++++++--------- 11 files changed, 180 insertions(+), 114 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart index d32f6e214..8b8ddf4ad 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_scroll_shadow/flutter_scroll_shadow.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/theme/fcc_theme.dart'; @@ -50,9 +51,13 @@ class BlockDialogueView extends StatelessWidget { children: [ ...parser.parse( '

${block.description.join(' ')}

', - fontColor: FccColors.gray05, - removeParagraphMargin: true, isSelectable: false, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( + color: FccColors.gray05, + ), + 'p': Style(margin: Margins.zero), + }, ), _buildToggleButton(context), _buildChallengeList(context, structure, dialogueHeaders), diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart index d209ebcad..3115d10a3 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_scroll_shadow/flutter_scroll_shadow.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/theme/fcc_theme.dart'; @@ -83,9 +84,13 @@ class BlockGridView extends StatelessWidget { ), ...parser.parse( '

${block.description.join(' ')}

', - fontColor: FccColors.gray05, - removeParagraphMargin: true, isSelectable: false, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( + color: FccColors.gray05, + ), + 'p': Style(margin: Margins.zero), + }, ), Row( children: [ diff --git a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart index 7462c7f0d..189dea8b0 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/theme/fcc_theme.dart'; import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; @@ -55,9 +56,13 @@ class BlockLinkView extends StatelessWidget { ), ...parser.parse( '

${block.description.join(' ')}

', - fontColor: FccColors.gray05, - removeParagraphMargin: true, isSelectable: false, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( + color: FccColors.gray05, + ), + 'p': Style(margin: Margins.zero), + }, ), Row( children: [ diff --git a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart index 704671a06..cdb1febe9 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/theme/fcc_theme.dart'; import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; @@ -77,9 +78,13 @@ class BlockListView extends StatelessWidget { ), ...parser.parse( '

${block.description.join(' ')}

', - fontColor: FccColors.gray05, - removeParagraphMargin: true, isSelectable: false, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( + color: FccColors.gray05, + ), + 'p': Style(margin: Margins.zero), + }, ), Row( children: [ diff --git a/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart b/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart index bbd134c9d..1bbf17dc6 100644 --- a/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart +++ b/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:freecodecamp/enums/panel_type.dart'; import 'package:freecodecamp/models/learn/challenge_model.dart'; @@ -497,8 +498,12 @@ class ChallengeView extends StatelessWidget { final widgets = parser.parse( test.instruction, isSelectable: true, - removeParagraphMargin: true, - fontColor: FccColors.gray00, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( + color: FccColors.gray00, + ), + 'p': Style(margin: Margins.zero), + }, ); return ExpansionTile( backgroundColor: FccColors.gray90, diff --git a/mobile-app/lib/ui/views/learn/challenge/templates/english/english_view.dart b/mobile-app/lib/ui/views/learn/challenge/templates/english/english_view.dart index a5abf6bbe..551e8dff5 100644 --- a/mobile-app/lib/ui/views/learn/challenge/templates/english/english_view.dart +++ b/mobile-app/lib/ui/views/learn/challenge/templates/english/english_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:freecodecamp/models/learn/challenge_model.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/theme/fcc_theme.dart'; @@ -45,11 +46,17 @@ class EnglishView extends StatelessWidget { children: [ ...parser.parse( challenge.instructions, - fontColor: FccColors.gray05, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': + Style(color: FccColors.gray05), + }, ), ...parser.parse( challenge.description, - fontColor: FccColors.gray05, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': + Style(color: FccColors.gray05), + }, ), ], ), diff --git a/mobile-app/lib/ui/views/learn/challenge/templates/quiz/quiz_view.dart b/mobile-app/lib/ui/views/learn/challenge/templates/quiz/quiz_view.dart index bc4a77776..f7f87c839 100644 --- a/mobile-app/lib/ui/views/learn/challenge/templates/quiz/quiz_view.dart +++ b/mobile-app/lib/ui/views/learn/challenge/templates/quiz/quiz_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:freecodecamp/extensions/i18n_extension.dart'; import 'package:freecodecamp/models/learn/challenge_model.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; @@ -85,11 +86,17 @@ class QuizView extends StatelessWidget { children: [ ...parser.parse( challenge.instructions, - fontColor: FccColors.gray05, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': + Style(color: FccColors.gray05), + }, ), ...parser.parse( challenge.description, - fontColor: FccColors.gray05, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': + Style(color: FccColors.gray05), + }, ), ], ), diff --git a/mobile-app/lib/ui/views/learn/challenge/templates/review/review_viewmodel.dart b/mobile-app/lib/ui/views/learn/challenge/templates/review/review_viewmodel.dart index 8b13e3e7b..d92e90466 100644 --- a/mobile-app/lib/ui/views/learn/challenge/templates/review/review_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/challenge/templates/review/review_viewmodel.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:freecodecamp/app/app.locator.dart'; import 'package:freecodecamp/models/learn/challenge_model.dart'; import 'package:freecodecamp/service/learn/learn_service.dart'; @@ -49,12 +50,20 @@ class ReviewViewmodel extends BaseViewModel { setParsedInstructions = parser.parse( challenge.instructions, - fontColor: FccColors.gray05, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( + color: FccColors.gray05, + ), + }, ); setParsedDescription = parser.parse( challenge.description, - fontColor: FccColors.gray05, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( + color: FccColors.gray05, + ), + }, ); } } diff --git a/mobile-app/lib/ui/views/learn/widgets/assignment_widget.dart b/mobile-app/lib/ui/views/learn/widgets/assignment_widget.dart index c5b39c19b..0c4325ac3 100644 --- a/mobile-app/lib/ui/views/learn/widgets/assignment_widget.dart +++ b/mobile-app/lib/ui/views/learn/widgets/assignment_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:freecodecamp/ui/views/news/html_handler/html_handler.dart'; class Assignment extends StatelessWidget { @@ -53,7 +54,14 @@ class Assignment extends StatelessWidget { children: parser.parse( label, isSelectable: false, - fontColor: value ? const Color(0xDEFFFFFF) : null, + customStyles: value + ? { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': + Style( + color: const Color(0xDEFFFFFF), + ), + } + : {}, ), ), ), diff --git a/mobile-app/lib/ui/views/learn/widgets/quiz_widget.dart b/mobile-app/lib/ui/views/learn/widgets/quiz_widget.dart index 2ddcb6ac9..0dc439848 100644 --- a/mobile-app/lib/ui/views/learn/widgets/quiz_widget.dart +++ b/mobile-app/lib/ui/views/learn/widgets/quiz_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:freecodecamp/models/learn/challenge_model.dart'; import 'package:freecodecamp/ui/theme/fcc_theme.dart'; import 'package:freecodecamp/ui/views/learn/widgets/challenge_card.dart'; @@ -81,7 +82,9 @@ class _QuizWidgetState extends State { .map((a) => parser.parse( a.answer, isSelectable: false, - removeParagraphMargin: true, + customStyles: { + 'p': Style(margin: Margins.zero), + }, )) .toList()) .toList(); @@ -216,7 +219,11 @@ class _QuizWidgetState extends State { feedbackWidgets.addAll( parser.parse( feedback, - fontColor: isCorrect == true ? FccColors.green40 : FccColors.red15, + customStyles: { + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( + color: isCorrect == true ? FccColors.green40 : FccColors.red15, + ), + }, ), ); } diff --git a/mobile-app/lib/ui/views/news/html_handler/html_handler.dart b/mobile-app/lib/ui/views/news/html_handler/html_handler.dart index 43d5b5bb3..edca04618 100644 --- a/mobile-app/lib/ui/views/news/html_handler/html_handler.dart +++ b/mobile-app/lib/ui/views/news/html_handler/html_handler.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html_table/flutter_html_table.dart'; import 'package:freecodecamp/ui/theme/fcc_theme.dart'; @@ -13,6 +13,89 @@ import 'package:phone_ide/phone_ide.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:youtube_player_iframe/youtube_player_iframe.dart'; +final Map defaultStyles = { + 'h1': Style( + margin: Margins.only(left: 2, top: 32, right: 2), + fontSize: FontSize.xxLarge, + ), + 'h2': Style( + margin: Margins.only(left: 2, top: 32, right: 2), + fontSize: FontSize.xxLarge, + ), + 'h3': Style( + margin: Margins.only(left: 2, top: 32, right: 2), + fontSize: FontSize.xLarge, + ), + 'h4': Style( + margin: Margins.only(left: 2, top: 32, right: 2), + fontSize: FontSize.large, + ), + 'h5': Style( + margin: Margins.only(left: 2, top: 32, right: 2), + fontSize: FontSize.large, + ), + 'h6': Style( + margin: Margins.only(left: 2, top: 32, right: 2), + fontSize: FontSize.large, + ), + '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( + fontSize: FontSize.xLarge, + color: Colors.white.withValues(alpha: 0.87), + ), + 'body': Style( + fontFamily: 'Lato', + padding: HtmlPaddings.only(left: 4, right: 4), + ), + 'strong': Style( + fontWeight: FontWeight.bold, + ), + 'a': Style( + color: Colors.white, + textDecoration: TextDecoration.underline, + textDecorationColor: Colors.white, + ), + 'p': Style( + fontSize: FontSize(20), + margin: Margins.only(top: 8, bottom: 8), + ), + 'li': Style( + margin: Margins.only(top: 8), + lineHeight: const LineHeight(1.5), + ), + 'td': Style( + border: const Border( + bottom: BorderSide(color: Colors.grey), + ), + padding: HtmlPaddings.all(12), + backgroundColor: Colors.white, + color: Colors.black, + fontSize: FontSize.medium, + ), + 'th': Style( + padding: HtmlPaddings.all(12), + backgroundColor: const Color.fromRGBO(0xdf, 0xdf, 0xe2, 1), + color: Colors.black, + ), + 'th strong': Style( + color: Colors.black, + fontSize: FontSize.medium, + ), + 'figure': Style( + margin: Margins.zero, + textAlign: TextAlign.center, + ), + 'figcaption': Style( + fontSize: FontSize.medium, + ), + 'code': Style( + fontFamily: 'Hack', + backgroundColor: FccColors.gray75, + color: Colors.white.withValues(alpha: 0.87), + fontSize: FontSize.xLarge, + ), + 'pre': Style(fontFamily: 'Hack') +}; + class HTMLParser { const HTMLParser({ Key? key, @@ -24,9 +107,7 @@ class HTMLParser { List parse( String html, { bool isSelectable = true, - bool removeParagraphMargin = false, - Color? fontColor, - double? fontSize, + Map customStyles = const {}, }) { dom.Document result = parser.parse(html); @@ -37,8 +118,7 @@ class HTMLParser { _parseHTMLWidget( result.body!.children[i].outerHtml, isSelectable, - removeParagraphMargin, - fontColor, + customStyles, ), ); } @@ -63,98 +143,21 @@ class HTMLParser { ); } - Widget _parseHTMLWidget(child, - [bool isSelectable = true, - bool removeParagraphMargin = false, - Color? fontColor, - double? fontSize]) { + Widget _parseHTMLWidget( + child, [ + bool isSelectable = true, + Map customStyles = const {}, + ]) { Html htmlWidget = Html( shrinkWrap: true, data: child, - style: { - 'h1': Style( - margin: Margins.only(left: 2, top: 32, right: 2), - fontSize: FontSize.xxLarge, - ), - 'h2': Style( - margin: Margins.only(left: 2, top: 32, right: 2), - fontSize: FontSize.xxLarge, - ), - 'h3': Style( - margin: Margins.only(left: 2, top: 32, right: 2), - fontSize: FontSize.xLarge, - ), - 'h4': Style( - margin: Margins.only(left: 2, top: 32, right: 2), - fontSize: FontSize.large, - ), - 'h5': Style( - margin: Margins.only(left: 2, top: 32, right: 2), - fontSize: FontSize.large, - ), - 'h6': Style( - margin: Margins.only(left: 2, top: 32, right: 2), - fontSize: FontSize.large, - ), - '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( - fontSize: FontSize.xLarge, - color: fontColor ?? Colors.white.withValues(alpha: 0.87), - ), - 'body': Style( - fontFamily: 'Lato', - padding: HtmlPaddings.only(left: 4, right: 4), - ), - 'strong': Style( - fontWeight: FontWeight.bold, - ), - 'a': Style( - color: Colors.white, - textDecoration: TextDecoration.underline, - textDecorationColor: Colors.white, - ), - 'p': Style( - fontSize: FontSize(fontSize ?? 20), - margin: removeParagraphMargin - ? Margins.all(0) - : Margins.only(top: 8, bottom: 8), - ), - 'li': Style( - margin: Margins.only(top: 8), - lineHeight: const LineHeight(1.5), - ), - 'td': Style( - border: const Border( - bottom: BorderSide(color: Colors.grey), - ), - padding: HtmlPaddings.all(12), - backgroundColor: Colors.white, - color: Colors.black, - fontSize: FontSize.medium, - ), - 'th': Style( - padding: HtmlPaddings.all(12), - backgroundColor: const Color.fromRGBO(0xdf, 0xdf, 0xe2, 1), - color: Colors.black, - ), - 'th strong': Style( - color: Colors.black, - fontSize: FontSize.medium, - ), - 'figure': Style( - margin: Margins.zero, - textAlign: TextAlign.center, - ), - 'figcaption': Style( - fontSize: FontSize.medium, - ), - 'code': Style( - fontFamily: 'Hack', - backgroundColor: FccColors.gray75, - color: Colors.white.withValues(alpha: 0.87), - fontSize: FontSize.xLarge, - ), - 'pre': Style(fontFamily: 'Hack') - }, + style: mergeMaps( + defaultStyles, + customStyles, + value: (s0, s1) { + return s0.merge(s1); + }, + ), onLinkTap: (url, attributes, element) { launchUrl(Uri.parse(url!.trim())); }, From d694cf391102615792bf78deabb2bd6210a543ec Mon Sep 17 00:00:00 2001 From: Niraj Nandish Date: Wed, 2 Jul 2025 07:35:52 +0530 Subject: [PATCH 3/3] fix: simplify css mastcher Co-authored-by: Sem Bauke --- .../lib/ui/views/learn/widgets/assignment_widget.dart | 6 ++---- mobile-app/lib/ui/views/learn/widgets/quiz_widget.dart | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/widgets/assignment_widget.dart b/mobile-app/lib/ui/views/learn/widgets/assignment_widget.dart index 0c4325ac3..f87a70f8a 100644 --- a/mobile-app/lib/ui/views/learn/widgets/assignment_widget.dart +++ b/mobile-app/lib/ui/views/learn/widgets/assignment_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:freecodecamp/ui/theme/fcc_theme.dart'; import 'package:freecodecamp/ui/views/news/html_handler/html_handler.dart'; class Assignment extends StatelessWidget { @@ -56,10 +57,7 @@ class Assignment extends StatelessWidget { isSelectable: false, customStyles: value ? { - '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': - Style( - color: const Color(0xDEFFFFFF), - ), + '*': Style(color: FccColors.gray00), } : {}, ), diff --git a/mobile-app/lib/ui/views/learn/widgets/quiz_widget.dart b/mobile-app/lib/ui/views/learn/widgets/quiz_widget.dart index 0dc439848..64285e337 100644 --- a/mobile-app/lib/ui/views/learn/widgets/quiz_widget.dart +++ b/mobile-app/lib/ui/views/learn/widgets/quiz_widget.dart @@ -220,7 +220,7 @@ class _QuizWidgetState extends State { parser.parse( feedback, customStyles: { - '*:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6)': Style( + '*:not(pre):not(code)': Style( color: isCorrect == true ? FccColors.green40 : FccColors.red15, ), },