import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; 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: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; const MyRecipeWidget({super.key, required this.recipe}); @override State createState() => _MyRecipeWidgetState(); } class _MyRecipeWidgetState extends State { late final int hours; late final int minutes; late final String timeTitle; @override void initState() { super.initState(); hours = widget.recipe.cookingTime ~/ 60; minutes = widget.recipe.cookingTime % 60; if (hours > 0) { timeTitle = '$hours ${getHourForm(hours)} $minutes ${getMinuteForm(minutes)}'; } else { timeTitle = '$minutes ${getMinuteForm(minutes)}'; } } @override Widget build(BuildContext context) { final s = S.of(context); return DecoratedBox( decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( children: [ InkWell( onTap: () async => GoRouter.of(context).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'navigateToComments': false}), child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( imageUrl: '${sl().baseUrl}/storage/${widget.recipe.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), ), ), Positioned( left: 8, bottom: 8, child: Wrap( runSpacing: 8, children: [ _iconButton('assets/icons/clock.svg', timeTitle), const SizedBox(width: 8), _iconButton('assets/icons/steps.svg', '${widget.recipe.steps?.length ?? 0} ${getStepForm(widget.recipe.steps?.length ?? 0)}'), const SizedBox(width: 8), _iconButton('assets/icons/calories.svg', '${widget.recipe.calories.toStringAsFixed(0)} ккал'), ], ), ), ], ), const SizedBox(height: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text(widget.recipe.title!, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), ), if (widget.recipe.description.isNotEmpty) ...[ const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Builder( builder: (context) { 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; return Html( data: md.markdownToHtml( description.replaceAll('\n', '
'), extensionSet: md.ExtensionSet.commonMark, ), 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), "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); } } }, ); }, ), ), ], ], ), const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ Row( children: [ Expanded( child: CustomOutlinedButton( onPressed: () async { final result = await sl().deleteRecipe(widget.recipe.slug); if (!result) { showErrorToast('Не удалось удалить рецепт'); return; } showSuccessToast('Рецепт удален'); if (context.mounted) context.read().getMyRecipes(); }, text: 'Удалить', height: 52, buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), buttonStyle: ButtonStyle( shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), side: const WidgetStatePropertyAll(BorderSide.none), ), ), ), const SizedBox(width: 8), Expanded( child: CustomOutlinedButton( onPressed: () async { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( uri: Uri.parse('${sl().baseUrl}/ru/recipe/${widget.recipe.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); }, height: 52, width: 52, buttonStyle: ButtonStyle( shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), side: const WidgetStatePropertyAll(BorderSide.none), ), text: s.share, buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), ), ), ], ), const SizedBox(height: 8), ], ), ), ], ), ); } Container _iconButton(String iconPath, [String? text]) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(16)), height: 32, child: Row( mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset(iconPath), if (text != null) const SizedBox(width: 8), if (text != null) Text( text, style: const TextStyle(fontSize: 12, color: Colors.black, fontWeight: FontWeight.w500), ), ], ), ); } } class _LinkRange { final int start; final int end; _LinkRange(this.start, this.end); }