diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart index ea4ccb97..70593f8c 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -238,10 +238,6 @@ class _BlogPostScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: MarkdownHtmlWidget( markdownText: blogPostState.blogPostModel!.description, - onLinkTap: (url, attributes, element) { - debugPrint('Link tapped: $url'); - return; - }, ), ), const SizedBox(height: 8), diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index 4b3428e6..6de38564 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -13,12 +13,10 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_html/flutter_html.dart'; +import 'package:eatvio_app/core/widgets/markdown_html_widget.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:markdown/markdown.dart' as md; -import 'package:url_launcher/url_launcher.dart'; class DietsWidget extends StatefulWidget { final Diet diet; @@ -117,94 +115,11 @@ class _DietsWidgetState extends State { String details = widget.diet.details.length > 200 ? '${widget.diet.details.substring(0, 200)}...' : widget.diet.details; - // Автоматически преобразуем URL в markdown ссылки - // Сначала исправляем неправильный синтаксис markdown ссылок - final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)'); - details = details.replaceAllMapped(wrongLinkRegex, (match) { - final urlInText = match.group(1)!; - if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) { - return match.group(0)!; - } - final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText; - return '[$urlInText]($fullUrl)'; - }); - - final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)'); - final existingLinks = <_LinkRange>[]; - for (final linkMatch in linkRegex.allMatches(details)) { - existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end)); - } - final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false); - String result = details; - int offset = 0; - for (final urlMatch in urlRegex.allMatches(details)) { - bool isInsideLink = existingLinks.any((link) => - urlMatch.start >= link.start && urlMatch.end <= link.end); - if (!isInsideLink) { - final url = urlMatch.group(0)!; - final fullUrl = url.startsWith('www.') ? 'https://$url' : url; - final replacement = '[$url]($fullUrl)'; - result = result.replaceRange( - urlMatch.start + offset, - urlMatch.end + offset, - replacement, - ); - offset += replacement.length - url.length; - } - } - details = result; - // Гарантируем, что жирный текст будет распознан - final boldRegex = RegExp(r'\*\*([^*]+)\*\*'); - details = details.replaceAllMapped(boldRegex, (match) { - return '${match.group(1)}'; - }); - - return Html( - data: md.markdownToHtml( - details, - extensionSet: md.ExtensionSet.gitHubFlavored, - ), - style: { - "body": Style( - margin: Margins.zero, - padding: HtmlPaddings.zero, - fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), - lineHeight: const LineHeight(1.5), - ), - "p": Style(margin: Margins.only(bottom: 12), color: appTheme.black), - "b, strong": Style(fontWeight: FontWeight.bold, color: appTheme.black), - "h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold), - "h1": Style(fontSize: FontSize(24)), - "h2": Style(fontSize: FontSize(20)), - "h3": Style(fontSize: FontSize(18)), - "ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)), - "li": Style(margin: Margins.only(bottom: 4), color: appTheme.black), - "strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black), - "em, i": Style(fontStyle: FontStyle.italic), - "code": Style( - backgroundColor: Colors.grey.shade100, - padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2), - fontFamily: 'monospace', - ), - "pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)), - "blockquote": Style( - padding: HtmlPaddings.only(left: 16), - margin: Margins.only(left: 8, bottom: 12), - fontStyle: FontStyle.italic, - color: Colors.grey.shade700, - ), - "a": Style(color: Colors.blue, textDecoration: TextDecoration.underline), - "img": Style(width: Width(MediaQuery.of(context).size.width - 32)), - }, - onLinkTap: (url, attributes, element) async { - if (url != null) { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } - }, + return MarkdownHtmlWidget( + markdownText: details, + fontSize: TextStyles.textSMedium.fontSize ?? 14, + textColor: appTheme.black, ); }, ), @@ -275,8 +190,3 @@ class _DietsWidgetState extends State { } } -class _LinkRange { - final int start; - final int end; - _LinkRange(this.start, this.end); -} diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index ed89b183..6d9e800a 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -16,12 +16,10 @@ import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_html/flutter_html.dart'; +import 'package:eatvio_app/core/widgets/markdown_html_widget.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:markdown/markdown.dart' as md; -import 'package:url_launcher/url_launcher.dart'; class RecipesWidget extends StatefulWidget { final int index; @@ -155,96 +153,11 @@ class _RecipesWidgetState extends State { String description = widget.recipe.description.length > 200 ? widget.recipe.description.substring(0, 200) : widget.recipe.description; - // Автоматически преобразуем URL в markdown ссылки - // Сначала исправляем неправильный синтаксис markdown ссылок - final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)'); - description = description.replaceAllMapped(wrongLinkRegex, (match) { - final urlInText = match.group(1)!; - if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) { - return match.group(0)!; - } - final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText; - return '[$urlInText]($fullUrl)'; - }); - - // Находим все существующие markdown ссылки и их позиции - final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)'); - final existingLinks = <_LinkRange>[]; - for (final linkMatch in linkRegex.allMatches(description)) { - existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end)); - } - - final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false); - String result = description; - int offset = 0; - for (final urlMatch in urlRegex.allMatches(description)) { - bool isInsideLink = existingLinks.any((link) => - urlMatch.start >= link.start && urlMatch.end <= link.end); - if (!isInsideLink) { - final url = urlMatch.group(0)!; - final fullUrl = url.startsWith('www.') ? 'https://$url' : url; - final replacement = '[$url]($fullUrl)'; - result = result.replaceRange( - urlMatch.start + offset, - urlMatch.end + offset, - replacement, - ); - offset += replacement.length - url.length; - } - } - description = result; - // Гарантируем, что жирный текст будет распознан - final boldRegex = RegExp(r'\*\*([^*]+)\*\*'); - description = description.replaceAllMapped(boldRegex, (match) { - return '${match.group(1)}'; - }); - - return Html( - data: md.markdownToHtml( - description, - extensionSet: md.ExtensionSet.gitHubFlavored, - ), - style: { - "body": Style( - margin: Margins.zero, - padding: HtmlPaddings.zero, - fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), - lineHeight: const LineHeight(1.5), - ), - "p": Style(margin: Margins.only(bottom: 12), color: appTheme.black), - "b, strong": Style(fontWeight: FontWeight.bold, color: appTheme.black), - "h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold), - "h1": Style(fontSize: FontSize(24)), - "h2": Style(fontSize: FontSize(20)), - "h3": Style(fontSize: FontSize(18)), - "ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)), - "li": Style(margin: Margins.only(bottom: 4), color: appTheme.black), - "strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black), - "em, i": Style(fontStyle: FontStyle.italic), - "code": Style( - backgroundColor: Colors.grey.shade100, - padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2), - fontFamily: 'monospace', - ), - "pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)), - "blockquote": Style( - padding: HtmlPaddings.only(left: 16), - margin: Margins.only(left: 8, bottom: 12), - fontStyle: FontStyle.italic, - color: Colors.grey.shade700, - ), - "a": Style(color: Colors.blue, textDecoration: TextDecoration.underline), - "img": Style(width: Width(MediaQuery.of(context).size.width - 32)), - }, - onLinkTap: (url, attributes, element) async { - if (url != null) { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } - }, + return MarkdownHtmlWidget( + markdownText: description, + fontSize: TextStyles.textSMedium.fontSize ?? 14, + textColor: appTheme.black, ); }, ), @@ -338,8 +251,3 @@ class _RecipesWidgetState extends State { } } -class _LinkRange { - final int start; - final int end; - _LinkRange(this.start, this.end); -} diff --git a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart index f655b8b9..f2f5892e 100644 --- a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -21,12 +21,10 @@ import 'package:eatvio_app/features/personal_diet_suggestion/presentation/person import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_html/flutter_html.dart'; +import 'package:eatvio_app/core/widgets/markdown_html_widget.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:markdown/markdown.dart' as md; -import 'package:url_launcher/url_launcher.dart'; class MyDietWidget extends StatefulWidget { final Diet diet; @@ -86,94 +84,11 @@ class _MyDietWidgetState extends State { String details = widget.diet.details.length > 200 ? '${widget.diet.details.substring(0, 200)}...' : widget.diet.details; - // Автоматически преобразуем URL в markdown ссылки - // Сначала исправляем неправильный синтаксис markdown ссылок - final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)'); - details = details.replaceAllMapped(wrongLinkRegex, (match) { - final urlInText = match.group(1)!; - if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) { - return match.group(0)!; - } - final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText; - return '[$urlInText]($fullUrl)'; - }); - - final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)'); - final existingLinks = <_LinkRange>[]; - for (final linkMatch in linkRegex.allMatches(details)) { - existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end)); - } - final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false); - String result = details; - int offset = 0; - for (final urlMatch in urlRegex.allMatches(details)) { - bool isInsideLink = existingLinks.any((link) => - urlMatch.start >= link.start && urlMatch.end <= link.end); - if (!isInsideLink) { - final url = urlMatch.group(0)!; - final fullUrl = url.startsWith('www.') ? 'https://$url' : url; - final replacement = '[$url]($fullUrl)'; - result = result.replaceRange( - urlMatch.start + offset, - urlMatch.end + offset, - replacement, - ); - offset += replacement.length - url.length; - } - } - details = result; - // Гарантируем, что жирный текст будет распознан - final boldRegex = RegExp(r'\*\*([^*]+)\*\*'); - details = details.replaceAllMapped(boldRegex, (match) { - return '${match.group(1)}'; - }); - - return Html( - data: md.markdownToHtml( - details, - extensionSet: md.ExtensionSet.gitHubFlavored, - ), - style: { - "body": Style( - margin: Margins.zero, - padding: HtmlPaddings.zero, - fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), - lineHeight: const LineHeight(1.5), - ), - "p": Style(margin: Margins.only(bottom: 12), color: appTheme.black), - "b, strong": Style(fontWeight: FontWeight.bold, color: appTheme.black), - "h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold), - "h1": Style(fontSize: FontSize(24)), - "h2": Style(fontSize: FontSize(20)), - "h3": Style(fontSize: FontSize(18)), - "ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)), - "li": Style(margin: Margins.only(bottom: 4), color: appTheme.black), - "strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black), - "em, i": Style(fontStyle: FontStyle.italic), - "code": Style( - backgroundColor: Colors.grey.shade100, - padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2), - fontFamily: 'monospace', - ), - "pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)), - "blockquote": Style( - padding: HtmlPaddings.only(left: 16), - margin: Margins.only(left: 8, bottom: 12), - fontStyle: FontStyle.italic, - color: Colors.grey.shade700, - ), - "a": Style(color: Colors.blue, textDecoration: TextDecoration.underline), - "img": Style(width: Width(MediaQuery.of(context).size.width - 32)), - }, - onLinkTap: (url, attributes, element) async { - if (url != null) { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } - }, + return MarkdownHtmlWidget( + markdownText: details, + fontSize: TextStyles.textSMedium.fontSize ?? 14, + textColor: appTheme.black, ); }, ), @@ -331,8 +246,3 @@ class _MyDietWidgetState extends State { } } -class _LinkRange { - final int start; - final int end; - _LinkRange(this.start, this.end); -} diff --git a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart index 5a53d76c..6c006f34 100644 --- a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart +++ b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart @@ -14,12 +14,10 @@ import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.d import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_html/flutter_html.dart'; +import 'package:eatvio_app/core/widgets/markdown_html_widget.dart'; // Removed SVG icon usage after switching to text buttons import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:markdown/markdown.dart' as md; -import 'package:url_launcher/url_launcher.dart'; class MyPlanWidget extends StatefulWidget { final PlanModel plan; @@ -78,94 +76,11 @@ class _MyPlanWidgetState extends State { String details = widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200)}...' : widget.plan.details; - // Автоматически преобразуем URL в markdown ссылки - // Сначала исправляем неправильный синтаксис markdown ссылок - final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)'); - details = details.replaceAllMapped(wrongLinkRegex, (match) { - final urlInText = match.group(1)!; - if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) { - return match.group(0)!; - } - final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText; - return '[$urlInText]($fullUrl)'; - }); - - final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)'); - final existingLinks = <_LinkRange>[]; - for (final linkMatch in linkRegex.allMatches(details)) { - existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end)); - } - final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false); - String result = details; - int offset = 0; - for (final urlMatch in urlRegex.allMatches(details)) { - bool isInsideLink = existingLinks.any((link) => - urlMatch.start >= link.start && urlMatch.end <= link.end); - if (!isInsideLink) { - final url = urlMatch.group(0)!; - final fullUrl = url.startsWith('www.') ? 'https://$url' : url; - final replacement = '[$url]($fullUrl)'; - result = result.replaceRange( - urlMatch.start + offset, - urlMatch.end + offset, - replacement, - ); - offset += replacement.length - url.length; - } - } - details = result; - // Гарантируем, что жирный текст будет распознан - final boldRegex = RegExp(r'\*\*([^*]+)\*\*'); - details = details.replaceAllMapped(boldRegex, (match) { - return '${match.group(1)}'; - }); - - return Html( - data: md.markdownToHtml( - details, - extensionSet: md.ExtensionSet.gitHubFlavored, - ), - style: { - "body": Style( - margin: Margins.zero, - padding: HtmlPaddings.zero, - fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), - lineHeight: const LineHeight(1.5), - ), - "p": Style(margin: Margins.only(bottom: 12), color: appTheme.black), - "b, strong": Style(fontWeight: FontWeight.bold, color: appTheme.black), - "h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold), - "h1": Style(fontSize: FontSize(24)), - "h2": Style(fontSize: FontSize(20)), - "h3": Style(fontSize: FontSize(18)), - "ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)), - "li": Style(margin: Margins.only(bottom: 4), color: appTheme.black), - "strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black), - "em, i": Style(fontStyle: FontStyle.italic), - "code": Style( - backgroundColor: Colors.grey.shade100, - padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2), - fontFamily: 'monospace', - ), - "pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)), - "blockquote": Style( - padding: HtmlPaddings.only(left: 16), - margin: Margins.only(left: 8, bottom: 12), - fontStyle: FontStyle.italic, - color: Colors.grey.shade700, - ), - "a": Style(color: Colors.blue, textDecoration: TextDecoration.underline), - "img": Style(width: Width(MediaQuery.of(context).size.width - 32)), - }, - onLinkTap: (url, attributes, element) async { - if (url != null) { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } - }, + return MarkdownHtmlWidget( + markdownText: details, + fontSize: TextStyles.textSMedium.fontSize ?? 14, + textColor: appTheme.black, ); }, ), @@ -276,8 +191,3 @@ class _MyPlanWidgetState extends State { } } -class _LinkRange { - final int start; - final int end; - _LinkRange(this.start, this.end); -} diff --git a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart index 43f05f43..e0e5cd01 100644 --- a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart +++ b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart @@ -13,10 +13,9 @@ import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/prof import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_html/flutter_html.dart'; +import 'package:eatvio_app/core/widgets/markdown_html_widget.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:markdown/markdown.dart' as md; import 'package:share_plus/share_plus.dart'; class ProfileBlogPostWidget extends StatefulWidget { @@ -144,47 +143,10 @@ class _ProfileBlogPostWidgetState extends State { description = description.substring(0, cutPoint); } - // Гарантируем, что жирный текст будет распознан - final boldRegex = RegExp(r'\*\*([^*]+)\*\*'); - description = description.replaceAllMapped(boldRegex, (match) { - return '${match.group(1)}'; - }); - - // Конвертируем markdown в HTML - String htmlData = md.markdownToHtml( - description, - extensionSet: md.ExtensionSet.gitHubFlavored, - ); - - return Html( - data: htmlData, - style: { - "body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5), color: Colors.black87), - "p": Style(margin: Margins.only(bottom: 12), color: Colors.black87), - "b, strong": Style(fontWeight: FontWeight.bold, color: Colors.black), - "h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold), - "h1": Style(fontSize: FontSize(24)), - "h2": Style(fontSize: FontSize(20)), - "h3": Style(fontSize: FontSize(18)), - "ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)), - "li": Style(margin: Margins.only(bottom: 4), color: Colors.black87), - "strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black), - "em, i": Style(fontStyle: FontStyle.italic), - "code": Style( - backgroundColor: Colors.grey.shade100, - padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2), - fontFamily: 'monospace', - ), - "pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)), - "blockquote": Style( - padding: HtmlPaddings.only(left: 16), - margin: Margins.only(left: 8, bottom: 12), - fontStyle: FontStyle.italic, - color: Colors.grey.shade700, - ), - "a": Style(color: Colors.blue, textDecoration: TextDecoration.underline), - "img": Style(width: Width(MediaQuery.of(context).size.width - 32)), - }, + return MarkdownHtmlWidget( + markdownText: description, + fontSize: 16, + textColor: Colors.black87, ); }, ), diff --git a/lib/features/recipe/presentation/pages/about_recipe_screen.dart b/lib/features/recipe/presentation/pages/about_recipe_screen.dart index b74a4201..a5448a86 100644 --- a/lib/features/recipe/presentation/pages/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/pages/about_recipe_screen.dart @@ -146,7 +146,7 @@ class _AboutRecipeScreenState extends State { // убираем true, чтобы padding учитывался корректно contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), // вертикальный паддинг увеличен - hintText: 'Комментарий', + hintText: s.comment_hint, hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)), filled: true, border: OutlineInputBorder( diff --git a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart index e253756d..c30aebc1 100644 --- a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart +++ b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart @@ -12,12 +12,10 @@ import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/m import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_html/flutter_html.dart'; +import 'package:eatvio_app/core/widgets/markdown_html_widget.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:markdown/markdown.dart' as md; import 'package:share_plus/share_plus.dart'; -import 'package:url_launcher/url_launcher.dart'; class MyRecipeWidget extends StatefulWidget { final MyRecipeModel recipe; @@ -100,94 +98,11 @@ class _MyRecipeWidgetState extends State { String description = widget.recipe.description.length > 200 ? '${widget.recipe.description.substring(0, 200)}...' : widget.recipe.description; - // Автоматически преобразуем URL в markdown ссылки - // Сначала исправляем неправильный синтаксис markdown ссылок - final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)'); - description = description.replaceAllMapped(wrongLinkRegex, (match) { - final urlInText = match.group(1)!; - if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) { - return match.group(0)!; - } - final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText; - return '[$urlInText]($fullUrl)'; - }); - - final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)'); - final existingLinks = <_LinkRange>[]; - for (final linkMatch in linkRegex.allMatches(description)) { - existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end)); - } - final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false); - String result = description; - int offset = 0; - for (final urlMatch in urlRegex.allMatches(description)) { - bool isInsideLink = existingLinks.any((link) => - urlMatch.start >= link.start && urlMatch.end <= link.end); - if (!isInsideLink) { - final url = urlMatch.group(0)!; - final fullUrl = url.startsWith('www.') ? 'https://$url' : url; - final replacement = '[$url]($fullUrl)'; - result = result.replaceRange( - urlMatch.start + offset, - urlMatch.end + offset, - replacement, - ); - offset += replacement.length - url.length; - } - } - description = result; - // Гарантируем, что жирный текст будет распознан - final boldRegex = RegExp(r'\*\*([^*]+)\*\*'); - description = description.replaceAllMapped(boldRegex, (match) { - return '${match.group(1)}'; - }); - - return Html( - data: md.markdownToHtml( - description, - extensionSet: md.ExtensionSet.gitHubFlavored, - ), - style: { - "body": Style( - margin: Margins.zero, - padding: HtmlPaddings.zero, - fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), - lineHeight: const LineHeight(1.5), - ), - "p": Style(margin: Margins.only(bottom: 12), color: appTheme.black), - "b, strong": Style(fontWeight: FontWeight.bold, color: appTheme.black), - "h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold), - "h1": Style(fontSize: FontSize(24)), - "h2": Style(fontSize: FontSize(20)), - "h3": Style(fontSize: FontSize(18)), - "ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)), - "li": Style(margin: Margins.only(bottom: 4), color: appTheme.black), - "strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black), - "em, i": Style(fontStyle: FontStyle.italic), - "code": Style( - backgroundColor: Colors.grey.shade100, - padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2), - fontFamily: 'monospace', - ), - "pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)), - "blockquote": Style( - padding: HtmlPaddings.only(left: 16), - margin: Margins.only(left: 8, bottom: 12), - fontStyle: FontStyle.italic, - color: Colors.grey.shade700, - ), - "a": Style(color: Colors.blue, textDecoration: TextDecoration.underline), - "img": Style(width: Width(MediaQuery.of(context).size.width - 32)), - }, - onLinkTap: (url, attributes, element) async { - if (url != null) { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } - }, + return MarkdownHtmlWidget( + markdownText: description, + fontSize: TextStyles.textSMedium.fontSize ?? 14, + textColor: appTheme.black, ); }, ), @@ -278,8 +193,3 @@ class _MyRecipeWidgetState extends State { } } -class _LinkRange { - final int start; - final int end; - _LinkRange(this.start, this.end); -}