diff --git a/lib/shared/widgets/markdown/markdown_config.dart b/lib/shared/widgets/markdown/markdown_config.dart index 68411cd..9879383 100644 --- a/lib/shared/widgets/markdown/markdown_config.dart +++ b/lib/shared/widgets/markdown/markdown_config.dart @@ -2,8 +2,9 @@ import 'dart:convert'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; -import 'package:markdown/markdown.dart' as md; +import 'package:gpt_markdown/custom_widgets/markdown_config.dart' + show CodeBlockBuilder, ImageBuilder; +import 'package:gpt_markdown/gpt_markdown.dart'; import 'package:conduit/l10n/app_localizations.dart'; @@ -11,16 +12,18 @@ import '../../theme/theme_extensions.dart'; class ConduitMarkdownTheme { const ConduitMarkdownTheme({ - required this.styleSheet, - required this.builders, + required this.textStyle, + required this.themeData, required this.imageBuilder, - this.inlineSyntaxes = const [], + required this.codeBuilder, + this.followLinkColor = true, }); - final MarkdownStyleSheet styleSheet; - final Map builders; - final MarkdownImageBuilder imageBuilder; - final List inlineSyntaxes; + final TextStyle textStyle; + final GptMarkdownThemeData themeData; + final ImageBuilder imageBuilder; + final CodeBlockBuilder codeBuilder; + final bool followLinkColor; } class ConduitMarkdownConfig { @@ -28,7 +31,6 @@ class ConduitMarkdownConfig { final theme = context.conduitTheme; final materialTheme = Theme.of(context); - final baseSheet = MarkdownStyleSheet.fromTheme(materialTheme); final bodyStyle = AppTypography.bodyMediumStyle.copyWith( color: theme.textPrimary, height: 1.45, @@ -36,43 +38,74 @@ class ConduitMarkdownConfig { final codeColor = theme.code?.color ?? theme.textSecondary; - final styleSheet = baseSheet.copyWith( - p: bodyStyle, + final markdownThemeData = GptMarkdownThemeData( + brightness: materialTheme.brightness, h1: AppTypography.headlineLargeStyle.copyWith(color: theme.textPrimary), h2: AppTypography.headlineMediumStyle.copyWith(color: theme.textPrimary), h3: AppTypography.headlineSmallStyle.copyWith(color: theme.textPrimary), - strong: bodyStyle.copyWith(fontWeight: FontWeight.w600), - em: bodyStyle.copyWith(fontStyle: FontStyle.italic), - blockquote: bodyStyle.copyWith( + h4: AppTypography.bodyLargeStyle.copyWith(color: theme.textPrimary), + h5: AppTypography.bodyMediumStyle.copyWith( color: theme.textSecondary, - fontStyle: FontStyle.italic, + fontWeight: FontWeight.w600, ), - code: AppTypography.codeStyle.copyWith(color: codeColor), - listBullet: bodyStyle, - tableBody: bodyStyle, - tableHead: bodyStyle.copyWith(fontWeight: FontWeight.w600), + h6: AppTypography.bodySmallStyle.copyWith(color: theme.textSecondary), + hrLineColor: theme.dividerColor, + highlightColor: theme.surfaceContainer.withValues(alpha: 0.4), + linkColor: materialTheme.colorScheme.primary, + linkHoverColor: materialTheme.colorScheme.primary.withValues(alpha: 0.8), ); - final builders = { - 'codeblock': _ConduitCodeBlockBuilder(theme), - }; - return ConduitMarkdownTheme( - styleSheet: styleSheet, - builders: builders, - imageBuilder: (uri, title, alt) { + textStyle: bodyStyle, + themeData: markdownThemeData, + imageBuilder: (context, imageUrl) { + final uri = Uri.tryParse(imageUrl); + if (uri == null) { + return _buildImageError(context, context.conduitTheme); + } + final scheme = uri.scheme; if (scheme == 'data') { - return buildBase64Image(uri.toString(), context, theme); + return buildBase64Image(imageUrl, context, context.conduitTheme); } if (scheme.isEmpty || scheme == 'http' || scheme == 'https') { - return buildNetworkImage(uri.toString(), context, theme); + return buildNetworkImage(imageUrl, context, context.conduitTheme); } return const SizedBox.shrink(); }, + codeBuilder: (context, name, code, closed) { + final conduitTheme = context.conduitTheme; + final textStyle = AppTypography.codeStyle.copyWith( + color: conduitTheme.code?.color ?? codeColor, + ); + + final container = Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: Spacing.xs), + padding: const EdgeInsets.all(Spacing.sm), + decoration: BoxDecoration( + color: conduitTheme.surfaceBackground.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(AppBorderRadius.md), + border: Border.all( + color: conduitTheme.cardBorder.withValues(alpha: 0.2), + width: BorderWidth.micro, + ), + ), + child: SelectableText(code, style: textStyle), + ); + + final language = name.trim().isEmpty ? null : name.trim(); + + return CodeBlockWrapper( + code: code, + language: language, + theme: conduitTheme, + child: container, + ); + }, ); } @@ -162,48 +195,6 @@ class ConduitMarkdownConfig { } } -class _ConduitCodeBlockBuilder extends MarkdownElementBuilder { - _ConduitCodeBlockBuilder(this.theme); - - final ConduitThemeExtension theme; - - @override - Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) { - final rawText = element.textContent; - final classAttribute = element.attributes['class']; - String? language; - if (classAttribute != null && classAttribute.startsWith('language-')) { - language = classAttribute.substring('language-'.length); - } - - final textStyle = (preferredStyle ?? AppTypography.codeStyle).copyWith( - color: theme.code?.color ?? theme.textSecondary, - ); - - final container = Container( - width: double.infinity, - margin: const EdgeInsets.symmetric(vertical: Spacing.xs), - padding: const EdgeInsets.all(Spacing.sm), - decoration: BoxDecoration( - color: theme.surfaceBackground.withValues(alpha: 0.6), - borderRadius: BorderRadius.circular(AppBorderRadius.md), - border: Border.all( - color: theme.cardBorder.withValues(alpha: 0.2), - width: BorderWidth.micro, - ), - ), - child: SelectableText(rawText, style: textStyle), - ); - - return CodeBlockWrapper( - code: rawText, - language: language, - theme: theme, - child: container, - ); - } -} - class CodeBlockWrapper extends StatelessWidget { const CodeBlockWrapper({ super.key, diff --git a/lib/shared/widgets/markdown/streaming_markdown_widget.dart b/lib/shared/widgets/markdown/streaming_markdown_widget.dart index b11057e..e4aec70 100644 --- a/lib/shared/widgets/markdown/streaming_markdown_widget.dart +++ b/lib/shared/widgets/markdown/streaming_markdown_widget.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:gpt_markdown/gpt_markdown.dart'; import 'markdown_config.dart'; +typedef MarkdownLinkTapCallback = void Function(String url, String title); + class StreamingMarkdownWidget extends StatelessWidget { const StreamingMarkdownWidget({ super.key, @@ -13,7 +15,7 @@ class StreamingMarkdownWidget extends StatelessWidget { final String content; final bool isStreaming; - final MarkdownTapLinkCallback? onTapLink; + final MarkdownLinkTapCallback? onTapLink; @override Widget build(BuildContext context) { @@ -23,15 +25,20 @@ class StreamingMarkdownWidget extends StatelessWidget { return isStreaming ? const SizedBox.shrink() : const SizedBox.shrink(); } - return MarkdownBody( - data: content, - styleSheet: markdownTheme.styleSheet, - softLineBreak: true, - selectable: true, - builders: markdownTheme.builders, - inlineSyntaxes: markdownTheme.inlineSyntaxes, - imageBuilder: markdownTheme.imageBuilder, - onTapLink: onTapLink, + final textScaler = MediaQuery.maybeOf(context)?.textScaler; + + return GptMarkdownTheme( + gptThemeData: markdownTheme.themeData, + child: GptMarkdown( + content, + style: markdownTheme.textStyle, + followLinkColor: markdownTheme.followLinkColor, + textDirection: Directionality.of(context), + textScaler: textScaler, + onLinkTap: onTapLink, + codeBuilder: markdownTheme.codeBuilder, + imageBuilder: markdownTheme.imageBuilder, + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index 15a7ac4..c2c6aec 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -443,14 +443,14 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_markdown_plus: - dependency: "direct main" + flutter_math_fork: + dependency: transitive description: - name: flutter_markdown_plus - sha256: "7f349c075157816da399216a4127096108fd08e1ac931e34e72899281db4113c" + name: flutter_math_fork + sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "0.7.4" flutter_native_splash: dependency: "direct dev" description: @@ -531,6 +531,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 + url: "https://pub.dev" + source: hosted + version: "2.2.1" flutter_test: dependency: "direct dev" description: flutter @@ -589,6 +597,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.8.1" + gpt_markdown: + dependency: "direct main" + description: + name: gpt_markdown + sha256: "8174983f2ed7d8576d25810913e3afe3f8ffdaa3172c0c823b7cfc289b67f380" + url: "https://pub.dev" + source: hosted + version: "1.1.4" graphs: dependency: transitive description: @@ -813,14 +829,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - markdown: - dependency: "direct main" - description: - name: markdown - sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" - url: "https://pub.dev" - source: hosted - version: "7.3.0" matcher: dependency: transitive description: @@ -861,6 +869,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.5.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" node_preamble: dependency: transitive description: @@ -909,6 +925,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -997,6 +1021,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -1482,6 +1514,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: @@ -1570,6 +1610,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cfb7a0a..2dd709a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,9 +29,8 @@ dependencies: hive_ce_flutter: ^2.3.2 shared_preferences: ^2.3.2 - # UI Components - Enhanced Markdown - flutter_markdown_plus: ^1.0.5 - markdown: ^7.2.1 + # UI Components - GPT Markdown + gpt_markdown: ^1.1.4 cached_network_image: ^3.3.1 socket_io_client: ^3.1.2 yaml: ^3.1.2