forked from eatvio/eatvio_app
refactor: replace HTML rendering with MarkdownHtmlWidget across various components for consistent markdown processing
This commit is contained in:
@@ -238,10 +238,6 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
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),
|
||||
|
||||
@@ -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<DietsWidget> {
|
||||
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 '<b>${match.group(1)}</b>';
|
||||
});
|
||||
|
||||
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<DietsWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkRange {
|
||||
final int start;
|
||||
final int end;
|
||||
_LinkRange(this.start, this.end);
|
||||
}
|
||||
|
||||
@@ -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<RecipesWidget> {
|
||||
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 '<b>${match.group(1)}</b>';
|
||||
});
|
||||
|
||||
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<RecipesWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkRange {
|
||||
final int start;
|
||||
final int end;
|
||||
_LinkRange(this.start, this.end);
|
||||
}
|
||||
|
||||
@@ -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<MyDietWidget> {
|
||||
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 '<b>${match.group(1)}</b>';
|
||||
});
|
||||
|
||||
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<MyDietWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkRange {
|
||||
final int start;
|
||||
final int end;
|
||||
_LinkRange(this.start, this.end);
|
||||
}
|
||||
|
||||
@@ -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<MyPlanWidget> {
|
||||
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 '<b>${match.group(1)}</b>';
|
||||
});
|
||||
|
||||
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<MyPlanWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkRange {
|
||||
final int start;
|
||||
final int end;
|
||||
_LinkRange(this.start, this.end);
|
||||
}
|
||||
|
||||
@@ -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<ProfileBlogPostWidget> {
|
||||
description = description.substring(0, cutPoint);
|
||||
}
|
||||
|
||||
// Гарантируем, что жирный текст будет распознан
|
||||
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
|
||||
description = description.replaceAllMapped(boldRegex, (match) {
|
||||
return '<b>${match.group(1)}</b>';
|
||||
});
|
||||
|
||||
// Конвертируем 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -146,7 +146,7 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
|
||||
// убираем 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(
|
||||
|
||||
@@ -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<MyRecipeWidget> {
|
||||
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 '<b>${match.group(1)}</b>';
|
||||
});
|
||||
|
||||
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<MyRecipeWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkRange {
|
||||
final int start;
|
||||
final int end;
|
||||
_LinkRange(this.start, this.end);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user