feat(personalized_feed): fixed photos rendering and fixed profile posts rendering

This commit is contained in:
misha
2026-07-23 15:17:01 +03:00
parent c8c8ade083
commit a6fe6afb80
13 changed files with 491 additions and 406 deletions

View File

@@ -2,6 +2,8 @@ import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:carousel_slider_plus/carousel_slider_plus.dart';
import 'package:eatvio_app/features/feed/models/post_feed_mapper.dart';
import 'package:eatvio_app/features/feed/models/post_feed_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/feed_video_player/feed_video_player_widget.dart';
import 'package:flutter/material.dart';
@@ -9,7 +11,7 @@ import 'package:flutter/material.dart';
class BlogPostMediaContent extends StatefulWidget {
const BlogPostMediaContent({
super.key,
required this.galleryUrls,
required this.galleryImages,
required this.imageUrl,
required this.videoEmbedUrl,
required this.currentGalleryIndex,
@@ -18,7 +20,7 @@ class BlogPostMediaContent extends StatefulWidget {
required this.videoUrlThumb,
});
final List<String> galleryUrls;
final List<FeedGalleryImage> galleryImages;
final String imageUrl;
final String? videoEmbedUrl;
final String? videoUrlThumb;
@@ -32,16 +34,36 @@ class BlogPostMediaContent extends StatefulWidget {
class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
static const _counterDisplayDuration = Duration(seconds: 1);
static const _carouselHeight = 260.0;
/// Landscape / video slide aspect (width / height).
static const _horizontalAspectRatio = 16 / 9;
/// Portrait slide aspect (width / height).
static const _verticalAspectRatio = 3 / 4;
bool _isCounterVisible = false;
Timer? _counterHideTimer;
bool get _hasVideo => widget.videoEmbedUrl != null && widget.videoEmbedUrl!.trim().isNotEmpty;
List<FeedGalleryImage> get _gallery {
return [
for (final image in widget.galleryImages)
if (PostFeedMapper.resolveMediaUrl(image.url) case final url when url.isNotEmpty)
FeedGalleryImage(
url: url,
orientation: image.orientation,
id: image.id,
position: image.position,
),
];
}
List<String> get _galleryUrls => _gallery.map((e) => e.url).toList();
int get _slideCount {
if (widget.galleryUrls.isNotEmpty) {
return widget.galleryUrls.length + (_hasVideo ? 1 : 0);
if (_gallery.isNotEmpty) {
return _gallery.length + (_hasVideo ? 1 : 0);
}
if (_hasVideo) {
return 1;
@@ -49,10 +71,24 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
return 0;
}
bool get _isCurrentSlideVideo => _hasVideo && widget.currentGalleryIndex == widget.galleryUrls.length;
bool get _isCurrentSlideVideo => _hasVideo && widget.currentGalleryIndex == _gallery.length;
bool get _shouldShowCounter => !_isCurrentSlideVideo || _isCounterVisible;
/// Height is fixed for the whole post from the first gallery image (web-like).
String get _postOrientation {
if (_gallery.isNotEmpty) {
return _gallery.first.orientation;
}
return 'horizontal';
}
double _heightForOrientation(String orientation, double width) {
final aspectRatio = orientation == 'vertical' ? _verticalAspectRatio : _horizontalAspectRatio;
final maxHeight = MediaQuery.sizeOf(context).height * 0.7;
return (width / aspectRatio).clamp(180.0, maxHeight);
}
@override
void dispose() {
_counterHideTimer?.cancel();
@@ -90,13 +126,13 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
return false;
}
Widget _imageSlide(String itemUrl, int itemIndex) {
Widget _imageSlide(String itemUrl, int itemIndex, double height) {
return GestureDetector(
onTap: () => widget.onOpenGallery(widget.galleryUrls, itemIndex),
onTap: () => widget.onOpenGallery(_galleryUrls, itemIndex),
child: CachedNetworkImage(
fit: BoxFit.cover,
width: MediaQuery.of(context).size.width,
height: _carouselHeight,
width: MediaQuery.sizeOf(context).width,
height: height,
imageUrl: itemUrl,
errorWidget: (context, str, object) => const SizedBox.shrink(),
),
@@ -107,7 +143,7 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
return FeedVideoPlayer(embedUrl: widget.videoEmbedUrl!, videoUrlThumb: widget.videoUrlThumb);
}
Widget _carousel({required int itemCount, required NullableIndexedWidgetBuilder itemBuilder}) {
Widget _carousel({required int itemCount, required NullableIndexedWidgetBuilder itemBuilder, required double height}) {
return Stack(
children: [
NotificationListener<ScrollNotification>(
@@ -117,7 +153,7 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
itemBuilder: (context, itemIndex, _) => itemBuilder(context, itemIndex) ?? const SizedBox.shrink(),
options: CarouselOptions(
viewportFraction: 1,
height: _carouselHeight,
height: height,
enableInfiniteScroll: false,
onPageChanged: (index, _) => widget.onGalleryChanged(index),
),
@@ -141,13 +177,18 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
@override
Widget build(BuildContext context) {
if (widget.galleryUrls.length > 1 || (widget.galleryUrls.length == 1 && _hasVideo)) {
final gallery = _gallery;
final width = MediaQuery.sizeOf(context).width;
final mediaHeight = _heightForOrientation(_postOrientation, width);
if (gallery.length > 1 || (gallery.length == 1 && _hasVideo)) {
final itemCount = _slideCount;
return _carousel(
itemCount: itemCount,
height: mediaHeight,
itemBuilder: (context, itemIndex) {
if (itemIndex < widget.galleryUrls.length) {
return _imageSlide(widget.galleryUrls[itemIndex], itemIndex);
if (itemIndex < gallery.length) {
return _imageSlide(gallery[itemIndex].url, itemIndex, mediaHeight);
}
if (_hasVideo) {
return _videoSlide();
@@ -157,14 +198,14 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
);
}
if (widget.galleryUrls.length == 1) {
return _imageSlide(widget.galleryUrls.first, 0);
if (gallery.length == 1) {
return _imageSlide(gallery.first.url, 0, mediaHeight);
}
if (_hasVideo) {
return SizedBox(
height: _carouselHeight,
width: MediaQuery.of(context).size.width,
height: mediaHeight,
width: width,
child: _videoSlide(),
);
}
@@ -176,7 +217,7 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
tag: widget.imageUrl,
child: CachedNetworkImage(
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
width: width,
imageUrl: widget.imageUrl,
errorWidget: (context, str, object) => const SizedBox.shrink(),
),

View File

@@ -78,7 +78,6 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
@override
Widget build(BuildContext context) {
final imageUrl = PostFeedMapper.resolveMediaUrl(post.image);
final galleryUrls = post.images.map((e) => PostFeedMapper.resolveMediaUrl(e.url)).where((e) => e.isNotEmpty).toList();
final embedUrl = post.videoEmbedUrl?.trim();
return InkWell(
@@ -95,7 +94,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
_BlogPostHeader(post: post),
_BlogPostTextContent(post: post),
BlogPostMediaContent(
galleryUrls: galleryUrls,
galleryImages: post.images,
imageUrl: imageUrl,
videoEmbedUrl: embedUrl != null && embedUrl.isNotEmpty ? embedUrl : null,
videoUrlThumb: post.videoUrlThumb,

View File

@@ -169,9 +169,8 @@ class _FeedVideoPlayerState extends State<FeedVideoPlayer> {
@override
Widget build(BuildContext context) {
final player = SizedBox(
height: 220,
width: MediaQuery.sizeOf(context).width,
final player = ColoredBox(
color: Colors.black,
child: Stack(
fit: StackFit.expand,
children: [

View File

@@ -22,10 +22,13 @@ class FeedVideoPreviewPlaceholder extends StatelessWidget {
return Stack(
fit: StackFit.expand,
children: [
const ColoredBox(color: Colors.black),
if (previewUrl != null && previewUrl!.isNotEmpty)
CachedNetworkImage(
imageUrl: previewUrl!,
fit: BoxFit.cover,
fit: BoxFit.contain,
width: double.infinity,
height: double.infinity,
errorWidget: (_, _, _) => const ColoredBox(color: Color(0xFF2C2C2C)),
)
else