From 8e4c61f03be42a7076b70c3febad1cf71c8d7f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Wed, 19 Nov 2025 08:51:46 +0100 Subject: [PATCH 1/5] build: Update pubspec.lock --- pubspec.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 99dfd21ca..5ff83a837 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1104,10 +1104,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mgrs_dart: dependency: transitive description: @@ -1829,26 +1829,26 @@ packages: dependency: transitive description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.11" + version: "0.6.12" timezone: dependency: transitive description: From d8d0abf27cbaf08fb09b2740a9e363834eaec5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Wed, 19 Nov 2025 09:47:41 +0100 Subject: [PATCH 2/5] refactor: Use own highlight rendering with working scrollbar and text selection --- lib/pages/chat/events/html_message.dart | 92 ++++++++++++++++++------- lib/utils/code_highlight_theme.dart | 40 +++++++++++ lib/utils/error_reporter.dart | 10 +-- pubspec.lock | 18 ++--- pubspec.yaml | 2 +- 5 files changed, 117 insertions(+), 45 deletions(-) create mode 100644 lib/utils/code_highlight_theme.dart diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index ae4997e40..ddb1a394f 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -2,13 +2,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; -import 'package:flutter_highlighter/flutter_highlighter.dart'; -import 'package:flutter_highlighter/themes/shades-of-purple.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:highlight/highlight.dart' show highlight; import 'package:html/dom.dart' as dom; import 'package:html/parser.dart' as parser; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/utils/code_highlight_theme.dart'; import 'package:fluffychat/utils/event_checkbox_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -137,6 +137,19 @@ class HtmlMessage extends StatelessWidget { ]; } + InlineSpan _renderCodeBlockNode(dom.Node node) { + if (node is! dom.Element) { + return TextSpan(text: node.text); + } + final style = atomOneDarkTheme[node.className.split('-').last] ?? + atomOneDarkTheme['root']; + + return TextSpan( + children: node.nodes.map(_renderCodeBlockNode).toList(), + style: style, + ); + } + /// Transforms a Node to an InlineSpan. InlineSpan _renderHtml( dom.Node node, @@ -334,33 +347,60 @@ class HtmlMessage extends StatelessWidget { ); case 'code': final isInline = node.parent?.localName != 'pre'; + final lang = node.className + .split(' ') + .singleWhereOrNull( + (className) => className.startsWith('language-'), + ) + ?.split('language-') + .last ?? + 'md'; + final highlightedHtml = + highlight.parse(node.text, language: lang).toHtml(); + final element = parser.parse(highlightedHtml).body; + if (element == null) { + return const TextSpan(text: 'Unable to render code block!'); + } + final controller = isInline ? null : ScrollController(); + return WidgetSpan( child: Material( - clipBehavior: Clip.hardEdge, - borderRadius: BorderRadius.circular(4), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: HighlightView( - node.text, - language: node.className - .split(' ') - .singleWhereOrNull( - (className) => className.startsWith('language-'), - ) - ?.split('language-') - .last ?? - 'md', - theme: shadesOfPurpleTheme, - padding: EdgeInsets.symmetric( - horizontal: 8, - vertical: isInline ? 0 : 8, - ), - textStyle: TextStyle( - fontSize: fontSize, - fontFamily: 'RobotoMono', - ), - ), + color: atomOneBackgroundColor, + shape: RoundedRectangleBorder( + side: const BorderSide(color: hightlightTextColor), + borderRadius: BorderRadius.circular(4), ), + child: isInline + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Text.rich( + TextSpan( + children: [_renderCodeBlockNode(element)], + ), + selectionColor: hightlightTextColor.withAlpha(128), + ), + ) + : RawScrollbar( + thumbVisibility: true, + trackVisibility: true, + controller: controller, + thumbColor: hightlightTextColor, + trackColor: hightlightTextColor.withAlpha(128), + thickness: 8, + child: SingleChildScrollView( + controller: controller, + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text.rich( + TextSpan( + children: [_renderCodeBlockNode(element)], + ), + selectionColor: hightlightTextColor.withAlpha(212), + ), + ), + ), + ), ), ); case 'img': diff --git a/lib/utils/code_highlight_theme.dart b/lib/utils/code_highlight_theme.dart new file mode 100644 index 000000000..ac3a19144 --- /dev/null +++ b/lib/utils/code_highlight_theme.dart @@ -0,0 +1,40 @@ +import 'package:flutter/widgets.dart'; + +const hightlightTextColor = Color(0xffabb2bf); +const atomOneBackgroundColor = Color(0xff282c34); +const atomOneDarkTheme = { + 'root': TextStyle(color: hightlightTextColor), + 'comment': TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic), + 'quote': TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic), + 'doctag': TextStyle(color: Color(0xffc678dd)), + 'keyword': TextStyle(color: Color(0xffc678dd)), + 'formula': TextStyle(color: Color(0xffc678dd)), + 'section': TextStyle(color: Color(0xffe06c75)), + 'name': TextStyle(color: Color(0xffe06c75)), + 'selector-tag': TextStyle(color: Color(0xffe06c75)), + 'deletion': TextStyle(color: Color(0xffe06c75)), + 'subst': TextStyle(color: Color(0xffe06c75)), + 'literal': TextStyle(color: Color(0xff56b6c2)), + 'string': TextStyle(color: Color(0xff98c379)), + 'regexp': TextStyle(color: Color(0xff98c379)), + 'addition': TextStyle(color: Color(0xff98c379)), + 'attribute': TextStyle(color: Color(0xff98c379)), + 'meta-string': TextStyle(color: Color(0xff98c379)), + 'built_in': TextStyle(color: Color(0xffe6c07b)), + 'attr': TextStyle(color: Color(0xffd19a66)), + 'variable': TextStyle(color: Color(0xffd19a66)), + 'template-variable': TextStyle(color: Color(0xffd19a66)), + 'type': TextStyle(color: Color(0xffd19a66)), + 'selector-class': TextStyle(color: Color(0xffd19a66)), + 'selector-attr': TextStyle(color: Color(0xffd19a66)), + 'selector-pseudo': TextStyle(color: Color(0xffd19a66)), + 'number': TextStyle(color: Color(0xffd19a66)), + 'symbol': TextStyle(color: Color(0xff61aeee)), + 'bullet': TextStyle(color: Color(0xff61aeee)), + 'link': TextStyle(color: Color(0xff61aeee)), + 'meta': TextStyle(color: Color(0xff61aeee)), + 'selector-id': TextStyle(color: Color(0xff61aeee)), + 'title': TextStyle(color: Color(0xff61aeee)), + 'emphasis': TextStyle(fontStyle: FontStyle.italic), + 'strong': TextStyle(fontWeight: FontWeight.bold), +}; diff --git a/lib/utils/error_reporter.dart b/lib/utils/error_reporter.dart index 9f3e7e86f..5e90afcb5 100644 --- a/lib/utils/error_reporter.dart +++ b/lib/utils/error_reporter.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_highlighter/flutter_highlighter.dart'; -import 'package:flutter_highlighter/themes/shades-of-purple.dart'; import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -40,10 +38,12 @@ class ErrorReporter { height: 256, width: 256, child: SingleChildScrollView( - child: HighlightView( + child: Text( text, - language: 'sh', - theme: shadesOfPurpleTheme, + style: const TextStyle( + fontSize: 14, + fontFamily: 'RobotoMono', + ), ), ), ), diff --git a/pubspec.lock b/pubspec.lock index 5ff83a837..965667782 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -483,14 +483,6 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.0" - flutter_highlighter: - dependency: "direct main" - description: - name: flutter_highlighter - sha256: "93173afd47a9ada53f3176371755e7ea4a1065362763976d06d6adfb4d946e10" - url: "https://pub.dev" - source: hosted - version: "0.1.1" flutter_linkify: dependency: "direct main" description: @@ -799,14 +791,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0" - highlighter: - dependency: transitive + highlight: + dependency: "direct main" description: - name: highlighter - sha256: "92180c72b9da8758e1acf39a45aa305a97dcfe2fdc8f3d1d2947c23f2772bfbc" + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.7.0" html: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index cc38297df..e133ad76e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,6 @@ dependencies: flutter: sdk: flutter flutter_foreground_task: ^9.1.0 - flutter_highlighter: ^0.1.1 flutter_linkify: ^6.0.0 flutter_local_notifications: ^19.5.0 flutter_localizations: @@ -45,6 +44,7 @@ dependencies: geolocator: ^14.0.2 go_router: ^17.0.0 handy_window: ^0.4.0 + highlight: ^0.7.0 html: ^0.15.4 http: ^1.6.0 image: ^4.1.7 From 6836cab40ed0ec33e8f74f7e78964569c2dcc189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Wed, 19 Nov 2025 10:24:58 +0100 Subject: [PATCH 3/5] chore: Increase padding for code blocks --- lib/pages/chat/events/html_message.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index ddb1a394f..57491fa6a 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -391,7 +391,10 @@ class HtmlMessage extends StatelessWidget { controller: controller, scrollDirection: Axis.horizontal, child: Padding( - padding: const EdgeInsets.all(4.0), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), child: Text.rich( TextSpan( children: [_renderCodeBlockNode(element)], From c3ec0491d1c5af44810f295694f8721815c46df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Wed, 19 Nov 2025 10:36:44 +0100 Subject: [PATCH 4/5] chore: Remove horizontal scrolling for code blocks --- lib/pages/chat/events/html_message.dart | 31 +++++++------------------ 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 57491fa6a..50001ad38 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -361,7 +361,6 @@ class HtmlMessage extends StatelessWidget { if (element == null) { return const TextSpan(text: 'Unable to render code block!'); } - final controller = isInline ? null : ScrollController(); return WidgetSpan( child: Material( @@ -380,28 +379,16 @@ class HtmlMessage extends StatelessWidget { selectionColor: hightlightTextColor.withAlpha(128), ), ) - : RawScrollbar( - thumbVisibility: true, - trackVisibility: true, - controller: controller, - thumbColor: hightlightTextColor, - trackColor: hightlightTextColor.withAlpha(128), - thickness: 8, - child: SingleChildScrollView( - controller: controller, - scrollDirection: Axis.horizontal, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 4.0, - ), - child: Text.rich( - TextSpan( - children: [_renderCodeBlockNode(element)], - ), - selectionColor: hightlightTextColor.withAlpha(212), - ), + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Text.rich( + TextSpan( + children: [_renderCodeBlockNode(element)], ), + selectionColor: hightlightTextColor.withAlpha(212), ), ), ), From 5262395340cd92f7b136ea0868fe61cb022d3b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Wed, 19 Nov 2025 10:37:42 +0100 Subject: [PATCH 5/5] chore: follow up Simplify codeblock code --- lib/pages/chat/events/html_message.dart | 30 +++++++++---------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 50001ad38..a023e412d 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -369,28 +369,20 @@ class HtmlMessage extends StatelessWidget { side: const BorderSide(color: hightlightTextColor), borderRadius: BorderRadius.circular(4), ), - child: isInline - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Text.rich( - TextSpan( - children: [_renderCodeBlockNode(element)], - ), - selectionColor: hightlightTextColor.withAlpha(128), - ), - ) - : Padding( - padding: const EdgeInsets.symmetric( + child: Padding( + padding: isInline + ? const EdgeInsets.symmetric(horizontal: 4.0) + : const EdgeInsets.symmetric( horizontal: 8.0, vertical: 4.0, ), - child: Text.rich( - TextSpan( - children: [_renderCodeBlockNode(element)], - ), - selectionColor: hightlightTextColor.withAlpha(212), - ), - ), + child: Text.rich( + TextSpan( + children: [_renderCodeBlockNode(element)], + ), + selectionColor: hightlightTextColor.withAlpha(128), + ), + ), ), ); case 'img':