import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; import 'package:eatvio_app/core/widgets/markdown_html_widget.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/models/post_feed_mapper.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_media_content.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; class BlogPostWidget extends StatefulWidget { final FeedBlogPostModel post; const BlogPostWidget({super.key, required this.post}); @override State createState() => _BlogPostWidgetState(); } class _BlogPostWidgetState extends State { late bool isLiked; late int totalLikes; int currentGalleryIndex = 0; FeedBlogPostModel get post => widget.post; @override void initState() { super.initState(); isLiked = post.wasLikedByMe; totalLikes = post.totalLikes; } @override void didUpdateWidget(covariant BlogPostWidget oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.post.id != post.id) { currentGalleryIndex = 0; isLiked = post.wasLikedByMe; totalLikes = post.totalLikes; return; } if (oldWidget.post.wasLikedByMe != post.wasLikedByMe || oldWidget.post.totalLikes != post.totalLikes) { isLiked = post.wasLikedByMe; totalLikes = post.totalLikes; } } void _openGallery(List imageUrls, int index) { context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: imageUrls, index: index)); } Future _toggleLike() async { final nextLiked = !isLiked; setState(() { isLiked = nextLiked; nextLiked ? totalLikes++ : totalLikes--; }); context.read().updateFeedBlogPostList(post, nextLiked); if (nextLiked) { await context.read().likePost(post.id); } else { await context.read().unlikedPost(post.id); } } @override Widget build(BuildContext context) { final imageUrl = PostFeedMapper.resolveMediaUrl(post.image); final embedUrl = post.videoEmbedUrl?.trim(); return InkWell( borderRadius: BorderRadius.circular(12), onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: post.id), child: Material( borderRadius: BorderRadius.circular(12), color: Colors.white, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 12), _BlogPostHeader(post: post), _BlogPostTextContent(post: post), BlogPostMediaContent( galleryImages: post.images, imageUrl: imageUrl, videoEmbedUrl: embedUrl != null && embedUrl.isNotEmpty ? embedUrl : null, videoUrlThumb: post.videoUrlThumb, currentGalleryIndex: currentGalleryIndex, onOpenGallery: _openGallery, onGalleryChanged: (index) { setState(() { currentGalleryIndex = index; }); }, ), const SizedBox(height: 12), _BlogPostActionsRow(post: post, isLiked: isLiked, totalLikes: totalLikes, onToggleLike: _toggleLike), const SizedBox(height: 8), ], ), ), ); } } class _BlogPostHeader extends StatelessWidget { const _BlogPostHeader({required this.post}); final FeedBlogPostModel post; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: post.author.authorSlug), child: CircleAvatar( radius: 20, backgroundColor: Colors.black12, foregroundImage: post.author.authorProfileIcon.isNotEmpty ? NetworkImage(PostFeedMapper.resolveMediaUrl(post.author.authorProfileIcon)) : null, backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( post.author.authorName, overflow: TextOverflow.ellipsis, style: const TextStyle(height: 1, color: Colors.black87, fontWeight: FontWeight.w500, fontSize: 16), ), const SizedBox(height: 4), Text( '${calculateDifference(post.createdAt)} в ${post.createdAt.hour.toString().padLeft(2, '0')}:${post.createdAt.minute.toString().padLeft(2, '0')}', style: const TextStyle(height: 1, color: Colors.black45, fontSize: 14), ), ], ), ), if (!(post.author.isEntitySubscribedByMe ?? false) && !post.author.authorIsMe) Padding( padding: const EdgeInsets.only(right: 8), child: SubscribeButtonWidget(slug: post.author.authorSlug), ), CustomPopup( isAuthor: post.author.authorIsMe, complainedContentType: ComplainedContent.posts, path: 'blog_post/${post.id}', id: post.id, feedBlogPostModel: post, userSlug: post.author.authorSlug, ), ], ), ); } } class _BlogPostTextContent extends StatelessWidget { const _BlogPostTextContent({required this.post}); final FeedBlogPostModel post; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (post.title.isNotEmpty) ...[ const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( post.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87), ), ), ], if (post.title.isEmpty) const SizedBox(height: 8), if (post.title.isNotEmpty && post.description.isNotEmpty) const SizedBox(height: 8), if (post.description.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: MarkdownHtmlWidget(markdownText: _truncateDescription(post.description)), ), if (post.description.length > 200) const Padding( padding: EdgeInsets.symmetric(horizontal: 12), child: Text( 'Показать ещё', style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600), ), ), if (post.title.isNotEmpty || post.description.isNotEmpty) const SizedBox(height: 8), ], ); } } class _BlogPostActionsRow extends StatelessWidget { const _BlogPostActionsRow({required this.post, required this.isLiked, required this.totalLikes, required this.onToggleLike}); final FeedBlogPostModel post; final bool isLiked; final int totalLikes; final Future Function() onToggleLike; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ FeedIconButton( icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded, text: totalLikes.toString(), iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null, backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null, textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null, onTap: onToggleLike, ), const SizedBox(width: 8), FeedIconButton( svgIcon: SvgPicture.asset('assets/icons/comment.svg', height: 20, width: 20, colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn)), text: post.totalComments.toString(), onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: post.id), ), const SizedBox(width: 8), FeedIconButton( svgIcon: SvgPicture.asset(height: 15, width: 12, 'assets/icons/repost.svg', colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn)), onTap: () { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( uri: Uri.parse('${sl().baseUrl}/ru/blog_post/${post.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); }, ), const Spacer(), Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500), const SizedBox(width: 4), Text( post.totalViews.toString(), style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400), ), ], ), ); } } String _truncateDescription(String description) { final isLong = description.length > 200; if (!isLong) return description; var cutPoint = 200; final preview = description.substring(0, cutPoint); final boldOpen = (preview.split('**').length - 1) ~/ 2; final boldClose = preview.split('**').length - 1 - boldOpen * 2; final hasUnclosedBold = boldOpen > boldClose; if (hasUnclosedBold) { final searchEnd = (cutPoint + 100).clamp(0, description.length); final boldClosePos = description.indexOf('**', cutPoint); if (boldClosePos != -1 && boldClosePos < searchEnd) { cutPoint = boldClosePos + 2; } else { cutPoint = description.lastIndexOf(' ', 200); if (cutPoint == -1 || cutPoint < 150) cutPoint = 200; } } else { cutPoint = description.lastIndexOf(' ', cutPoint); if (cutPoint == -1 || cutPoint < 150) cutPoint = 200; } return description.substring(0, cutPoint); }